mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
feat(sso-chat-deployment): added sso auth option for chat deployment (#1729)
* feat(sso-chat-deployment): added sso auth option for chat deployment * ack PR comments
This commit is contained in:
@@ -71,6 +71,12 @@ export default function SSOForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-fill email if provided in URL (e.g., from deployed chat SSO)
|
||||||
|
const emailParam = searchParams.get('email')
|
||||||
|
if (emailParam) {
|
||||||
|
setEmail(emailParam)
|
||||||
|
}
|
||||||
|
|
||||||
// Check for SSO error from redirect
|
// Check for SSO error from redirect
|
||||||
const error = searchParams.get('error')
|
const error = searchParams.get('error')
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const chatUpdateSchema = z.object({
|
|||||||
imageUrl: z.string().optional(),
|
imageUrl: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
authType: z.enum(['public', 'password', 'email']).optional(),
|
authType: z.enum(['public', 'password', 'email', 'sso']).optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
allowedEmails: z.array(z.string()).optional(),
|
allowedEmails: z.array(z.string()).optional(),
|
||||||
outputConfigs: z
|
outputConfigs: z
|
||||||
@@ -165,7 +165,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
updateData.allowedEmails = []
|
updateData.allowedEmails = []
|
||||||
} else if (authType === 'password') {
|
} else if (authType === 'password') {
|
||||||
updateData.allowedEmails = []
|
updateData.allowedEmails = []
|
||||||
} else if (authType === 'email') {
|
} else if (authType === 'email' || authType === 'sso') {
|
||||||
updateData.password = null
|
updateData.password = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const chatSchema = z.object({
|
|||||||
welcomeMessage: z.string(),
|
welcomeMessage: z.string(),
|
||||||
imageUrl: z.string().optional(),
|
imageUrl: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
authType: z.enum(['public', 'password', 'email']).default('public'),
|
authType: z.enum(['public', 'password', 'email', 'sso']).default('public'),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
allowedEmails: z.array(z.string()).optional().default([]),
|
allowedEmails: z.array(z.string()).optional().default([]),
|
||||||
outputConfigs: z
|
outputConfigs: z
|
||||||
@@ -98,6 +98,13 @@ export async function POST(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authType === 'sso' && (!Array.isArray(allowedEmails) || allowedEmails.length === 0)) {
|
||||||
|
return createErrorResponse(
|
||||||
|
'At least one email or domain is required when using SSO access control',
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if identifier is available
|
// Check if identifier is available
|
||||||
const existingIdentifier = await db
|
const existingIdentifier = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -163,7 +170,7 @@ export async function POST(request: NextRequest) {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
authType,
|
authType,
|
||||||
password: encryptedPassword,
|
password: encryptedPassword,
|
||||||
allowedEmails: authType === 'email' ? allowedEmails : [],
|
allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [],
|
||||||
outputConfigs,
|
outputConfigs,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|||||||
@@ -262,7 +262,67 @@ export async function validateChatAuth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown auth type
|
if (authType === 'sso') {
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
return { authorized: false, error: 'auth_required_sso' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!parsedBody) {
|
||||||
|
return { authorized: false, error: 'SSO authentication is required' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, input, checkSSOAccess } = parsedBody
|
||||||
|
|
||||||
|
if (checkSSOAccess) {
|
||||||
|
if (!email) {
|
||||||
|
return { authorized: false, error: 'Email is required' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedEmails = deployment.allowedEmails || []
|
||||||
|
|
||||||
|
if (allowedEmails.includes(email)) {
|
||||||
|
return { authorized: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = email.split('@')[1]
|
||||||
|
if (domain && allowedEmails.some((allowed: string) => allowed === `@${domain}`)) {
|
||||||
|
return { authorized: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { authorized: false, error: 'Email not authorized for SSO access' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { auth } = await import('@/lib/auth')
|
||||||
|
const session = await auth.api.getSession({ headers: request.headers })
|
||||||
|
|
||||||
|
if (!session || !session.user) {
|
||||||
|
return { authorized: false, error: 'auth_required_sso' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const userEmail = session.user.email
|
||||||
|
if (!userEmail) {
|
||||||
|
return { authorized: false, error: 'SSO session does not contain email' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedEmails = deployment.allowedEmails || []
|
||||||
|
|
||||||
|
if (allowedEmails.includes(userEmail)) {
|
||||||
|
return { authorized: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = userEmail.split('@')[1]
|
||||||
|
if (domain && allowedEmails.some((allowed: string) => allowed === `@${domain}`)) {
|
||||||
|
return { authorized: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { authorized: false, error: 'Your email is not authorized to access this chat' }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${requestId}] Error validating SSO:`, error)
|
||||||
|
return { authorized: false, error: 'SSO authentication error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { authorized: false, error: 'Unsupported authentication type' }
|
return { authorized: false, error: 'Unsupported authentication type' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ChatMessageContainer,
|
ChatMessageContainer,
|
||||||
EmailAuth,
|
EmailAuth,
|
||||||
PasswordAuth,
|
PasswordAuth,
|
||||||
|
SSOAuth,
|
||||||
VoiceInterface,
|
VoiceInterface,
|
||||||
} from '@/app/chat/components'
|
} from '@/app/chat/components'
|
||||||
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants'
|
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants'
|
||||||
@@ -32,7 +33,7 @@ interface ChatConfig {
|
|||||||
welcomeMessage?: string
|
welcomeMessage?: string
|
||||||
headerText?: string
|
headerText?: string
|
||||||
}
|
}
|
||||||
authType?: 'public' | 'password' | 'email'
|
authType?: 'public' | 'password' | 'email' | 'sso'
|
||||||
outputConfigs?: Array<{ blockId: string; path?: string }>
|
outputConfigs?: Array<{ blockId: string; path?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
|
|||||||
const [userHasScrolled, setUserHasScrolled] = useState(false)
|
const [userHasScrolled, setUserHasScrolled] = useState(false)
|
||||||
const isUserScrollingRef = useRef(false)
|
const isUserScrollingRef = useRef(false)
|
||||||
|
|
||||||
const [authRequired, setAuthRequired] = useState<'password' | 'email' | null>(null)
|
const [authRequired, setAuthRequired] = useState<'password' | 'email' | 'sso' | null>(null)
|
||||||
|
|
||||||
const [isVoiceFirstMode, setIsVoiceFirstMode] = useState(false)
|
const [isVoiceFirstMode, setIsVoiceFirstMode] = useState(false)
|
||||||
const { isStreamingResponse, abortControllerRef, stopStreaming, handleStreamedResponse } =
|
const { isStreamingResponse, abortControllerRef, stopStreaming, handleStreamedResponse } =
|
||||||
@@ -222,6 +223,10 @@ export default function ChatClient({ identifier }: { identifier: string }) {
|
|||||||
setAuthRequired('email')
|
setAuthRequired('email')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (errorData.error === 'auth_required_sso') {
|
||||||
|
setAuthRequired('sso')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to load chat configuration: ${response.status}`)
|
throw new Error(`Failed to load chat configuration: ${response.status}`)
|
||||||
@@ -500,6 +505,16 @@ export default function ChatClient({ identifier }: { identifier: string }) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (authRequired === 'sso') {
|
||||||
|
return (
|
||||||
|
<SSOAuth
|
||||||
|
identifier={identifier}
|
||||||
|
onAuthSuccess={handleAuthSuccess}
|
||||||
|
title={title}
|
||||||
|
primaryColor={primaryColor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading state while fetching config using the extracted component
|
// Loading state while fetching config using the extracted component
|
||||||
|
|||||||
209
apps/sim/app/chat/components/auth/sso/sso-auth.tsx
Normal file
209
apps/sim/app/chat/components/auth/sso/sso-auth.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { type KeyboardEvent, useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { quickValidateEmail } from '@/lib/email/validation'
|
||||||
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import Nav from '@/app/(landing)/components/nav/nav'
|
||||||
|
import { inter } from '@/app/fonts/inter'
|
||||||
|
import { soehne } from '@/app/fonts/soehne/soehne'
|
||||||
|
|
||||||
|
const logger = createLogger('SSOAuth')
|
||||||
|
|
||||||
|
interface SSOAuthProps {
|
||||||
|
identifier: string
|
||||||
|
onAuthSuccess: () => void
|
||||||
|
title?: string
|
||||||
|
primaryColor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateEmailField = (emailValue: string): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
if (!emailValue || !emailValue.trim()) {
|
||||||
|
errors.push('Email is required.')
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = quickValidateEmail(emailValue.trim().toLowerCase())
|
||||||
|
if (!validation.isValid) {
|
||||||
|
errors.push(validation.reason || 'Please enter a valid email address.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SSOAuth({
|
||||||
|
identifier,
|
||||||
|
onAuthSuccess,
|
||||||
|
title = 'chat',
|
||||||
|
primaryColor = 'var(--brand-primary-hover-hex)',
|
||||||
|
}: SSOAuthProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||||
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
|
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkCustomBrand = () => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('auth-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('auth-button-gradient')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleAuthenticate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newEmail = e.target.value
|
||||||
|
setEmail(newEmail)
|
||||||
|
setShowEmailValidationError(false)
|
||||||
|
setEmailErrors([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAuthenticate = async () => {
|
||||||
|
const emailValidationErrors = validateEmailField(email)
|
||||||
|
setEmailErrors(emailValidationErrors)
|
||||||
|
setShowEmailValidationError(emailValidationErrors.length > 0)
|
||||||
|
|
||||||
|
if (emailValidationErrors.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkResponse = await fetch(`/api/chat/${identifier}`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, checkSSOAccess: true }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!checkResponse.ok) {
|
||||||
|
const errorData = await checkResponse.json()
|
||||||
|
setEmailErrors([errorData.error || 'Email not authorized for this chat'])
|
||||||
|
setShowEmailValidationError(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const callbackUrl = `/chat/${identifier}`
|
||||||
|
const ssoUrl = `/sso?email=${encodeURIComponent(email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||||
|
router.push(ssoUrl)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('SSO authentication error:', error)
|
||||||
|
setEmailErrors(['An error occurred during authentication'])
|
||||||
|
setShowEmailValidationError(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-white'>
|
||||||
|
<Nav variant='auth' />
|
||||||
|
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||||
|
<div className='w-full max-w-[410px]'>
|
||||||
|
<div className='flex flex-col items-center justify-center'>
|
||||||
|
{/* Header */}
|
||||||
|
<div className='space-y-1 text-center'>
|
||||||
|
<h1
|
||||||
|
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
|
||||||
|
>
|
||||||
|
SSO Authentication
|
||||||
|
</h1>
|
||||||
|
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||||
|
This chat requires SSO authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleAuthenticate()
|
||||||
|
}}
|
||||||
|
className={`${inter.className} mt-8 w-full space-y-8`}
|
||||||
|
>
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<Label htmlFor='email'>Work Email</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id='email'
|
||||||
|
name='email'
|
||||||
|
required
|
||||||
|
type='email'
|
||||||
|
autoCapitalize='none'
|
||||||
|
autoComplete='email'
|
||||||
|
autoCorrect='off'
|
||||||
|
placeholder='Enter your work email'
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className={cn(
|
||||||
|
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||||
|
showEmailValidationError &&
|
||||||
|
emailErrors.length > 0 &&
|
||||||
|
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||||
|
)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{showEmailValidationError && emailErrors.length > 0 && (
|
||||||
|
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||||
|
{emailErrors.map((error, index) => (
|
||||||
|
<p key={index}>{error}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Redirecting to SSO...' : 'Continue with SSO'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as EmailAuth } from './auth/email/email-auth'
|
export { default as EmailAuth } from './auth/email/email-auth'
|
||||||
export { default as PasswordAuth } from './auth/password/password-auth'
|
export { default as PasswordAuth } from './auth/password/password-auth'
|
||||||
|
export { default as SSOAuth } from './auth/sso/sso-auth'
|
||||||
export { ChatErrorState } from './error-state/error-state'
|
export { ChatErrorState } from './error-state/error-state'
|
||||||
export { ChatHeader } from './header/header'
|
export { ChatHeader } from './header/header'
|
||||||
export { ChatInput } from './input/input'
|
export { ChatInput } from './input/input'
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function ChatDeploy({
|
|||||||
(formData.authType !== 'password' ||
|
(formData.authType !== 'password' ||
|
||||||
Boolean(formData.password.trim()) ||
|
Boolean(formData.password.trim()) ||
|
||||||
Boolean(existingChat)) &&
|
Boolean(existingChat)) &&
|
||||||
(formData.authType !== 'email' || formData.emails.length > 0)
|
((formData.authType !== 'email' && formData.authType !== 'sso') || formData.emails.length > 0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onValidationChange?.(isFormValid)
|
onValidationChange?.(isFormValid)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw, Trash2 } from 'lucide-react'
|
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw, Trash2 } from 'lucide-react'
|
||||||
import { Button, Card, CardContent, Input, Label } from '@/components/ui'
|
import { Button, Card, CardContent, Input, Label } from '@/components/ui'
|
||||||
|
import { getEnv, isTruthy } from '@/lib/env'
|
||||||
import { cn, generatePassword } from '@/lib/utils'
|
import { cn, generatePassword } from '@/lib/utils'
|
||||||
import type { AuthType } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-chat-form'
|
import type { AuthType } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-chat-form'
|
||||||
|
|
||||||
@@ -63,13 +64,20 @@ export function AuthSelector({
|
|||||||
onEmailsChange(emails.filter((e) => e !== email))
|
onEmailsChange(emails.filter((e) => e !== email))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
|
||||||
|
const authOptions = ssoEnabled
|
||||||
|
? (['public', 'password', 'email', 'sso'] as const)
|
||||||
|
: (['public', 'password', 'email'] as const)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label className='font-medium text-sm'>Access Control</Label>
|
<Label className='font-medium text-sm'>Access Control</Label>
|
||||||
|
|
||||||
{/* Auth Type Selection */}
|
{/* Auth Type Selection */}
|
||||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-3'>
|
<div
|
||||||
{(['public', 'password', 'email'] as const).map((type) => (
|
className={cn('grid grid-cols-1 gap-3', ssoEnabled ? 'md:grid-cols-4' : 'md:grid-cols-3')}
|
||||||
|
>
|
||||||
|
{authOptions.map((type) => (
|
||||||
<Card
|
<Card
|
||||||
key={type}
|
key={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -92,11 +100,13 @@ export function AuthSelector({
|
|||||||
{type === 'public' && 'Public Access'}
|
{type === 'public' && 'Public Access'}
|
||||||
{type === 'password' && 'Password Protected'}
|
{type === 'password' && 'Password Protected'}
|
||||||
{type === 'email' && 'Email Access'}
|
{type === 'email' && 'Email Access'}
|
||||||
|
{type === 'sso' && 'SSO Access'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
{type === 'public' && 'Anyone can access your chat'}
|
{type === 'public' && 'Anyone can access your chat'}
|
||||||
{type === 'password' && 'Secure with a single password'}
|
{type === 'password' && 'Secure with a single password'}
|
||||||
{type === 'email' && 'Restrict to specific emails'}
|
{type === 'email' && 'Restrict to specific emails'}
|
||||||
|
{type === 'sso' && 'Authenticate via SSO provider'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -207,10 +217,12 @@ export function AuthSelector({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authType === 'email' && (
|
{(authType === 'email' || authType === 'sso') && (
|
||||||
<Card className='rounded-[8px] shadow-none'>
|
<Card className='rounded-[8px] shadow-none'>
|
||||||
<CardContent className='p-4'>
|
<CardContent className='p-4'>
|
||||||
<h3 className='mb-2 font-medium text-sm'>Email Access Settings</h3>
|
<h3 className='mb-2 font-medium text-sm'>
|
||||||
|
{authType === 'email' ? 'Email Access Settings' : 'SSO Access Settings'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Input
|
<Input
|
||||||
@@ -264,7 +276,9 @@ export function AuthSelector({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className='mt-2 text-muted-foreground text-xs'>
|
<p className='mt-2 text-muted-foreground text-xs'>
|
||||||
Add specific emails or entire domains (@example.com)
|
{authType === 'email'
|
||||||
|
? 'Add specific emails or entire domains (@example.com)'
|
||||||
|
: 'Add specific emails or entire domains (@example.com) that can access via SSO'}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ export function useChatDeployment() {
|
|||||||
},
|
},
|
||||||
authType: formData.authType,
|
authType: formData.authType,
|
||||||
password: formData.authType === 'password' ? formData.password : undefined,
|
password: formData.authType === 'password' ? formData.password : undefined,
|
||||||
allowedEmails: formData.authType === 'email' ? formData.emails : [],
|
allowedEmails:
|
||||||
|
formData.authType === 'email' || formData.authType === 'sso' ? formData.emails : [],
|
||||||
outputConfigs,
|
outputConfigs,
|
||||||
apiKey: deploymentInfo?.apiKey,
|
apiKey: deploymentInfo?.apiKey,
|
||||||
deployApiEnabled: !existingChatId, // Only deploy API for new chats
|
deployApiEnabled: !existingChatId, // Only deploy API for new chats
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
export type AuthType = 'public' | 'password' | 'email'
|
export type AuthType = 'public' | 'password' | 'email' | 'sso'
|
||||||
|
|
||||||
export interface ChatFormData {
|
export interface ChatFormData {
|
||||||
identifier: string
|
identifier: string
|
||||||
@@ -85,6 +85,10 @@ export function useChatForm(initialData?: Partial<ChatFormData>) {
|
|||||||
newErrors.emails = 'At least one email or domain is required when using email access control'
|
newErrors.emails = 'At least one email or domain is required when using email access control'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formData.authType === 'sso' && formData.emails.length === 0) {
|
||||||
|
newErrors.emails = 'At least one email or domain is required when using SSO access control'
|
||||||
|
}
|
||||||
|
|
||||||
if (formData.selectedOutputBlocks.length === 0) {
|
if (formData.selectedOutputBlocks.length === 0) {
|
||||||
newErrors.outputBlocks = 'Please select at least one output block'
|
newErrors.outputBlocks = 'Please select at least one output block'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -648,9 +648,9 @@ export const chat = pgTable(
|
|||||||
customizations: json('customizations').default('{}'), // For UI customization options
|
customizations: json('customizations').default('{}'), // For UI customization options
|
||||||
|
|
||||||
// Authentication options
|
// Authentication options
|
||||||
authType: text('auth_type').notNull().default('public'), // 'public', 'password', 'email'
|
authType: text('auth_type').notNull().default('public'), // 'public', 'password', 'email', 'sso'
|
||||||
password: text('password'), // Stored hashed, populated when authType is 'password'
|
password: text('password'), // Stored hashed, populated when authType is 'password'
|
||||||
allowedEmails: json('allowed_emails').default('[]'), // Array of allowed emails or domains when authType is 'email'
|
allowedEmails: json('allowed_emails').default('[]'), // Array of allowed emails or domains when authType is 'email' or 'sso'
|
||||||
|
|
||||||
// Output configuration
|
// Output configuration
|
||||||
outputConfigs: json('output_configs').default('[]'), // Array of {blockId, path} objects
|
outputConfigs: json('output_configs').default('[]'), // Array of {blockId, path} objects
|
||||||
|
|||||||
Reference in New Issue
Block a user