Files
sim/apps/sim/app/api/form/utils.ts
Waleed 6262503b89 feat(deployed-form): added deployed form input (#2679)
* feat(deployed-form): added deployed form input

* styling consolidation, finishing touches on form

* updated docs

* remove unused files with knip

* added more form fields

* consolidated more test utils

* remove unused/unneeded zustand stores, refactored stores for consistency

* improvement(files): uncolorized plan name

* feat(emcn): button-group

* feat(emcn): tag input, tooltip shortcut

* improvement(emcn): modal padding, api, chat, form

* fix: deleted migrations

* feat(form): added migrations

* fix(emcn): tag input

* fix: failing tests on build

* add suplementary hover and fix bg color in date picker

* fix: build errors

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
2026-01-09 23:42:21 -08:00

205 lines
5.6 KiB
TypeScript

import { db } from '@sim/db'
import { form, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { NextRequest, NextResponse } from 'next/server'
import {
isEmailAllowed,
setDeploymentAuthCookie,
validateAuthToken,
} from '@/lib/core/security/deployment'
import { decryptSecret } from '@/lib/core/security/encryption'
import { hasAdminPermission } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('FormAuthUtils')
export function setFormAuthCookie(
response: NextResponse,
formId: string,
type: string,
encryptedPassword?: string | null
): void {
setDeploymentAuthCookie(response, 'form', formId, type, encryptedPassword)
}
/**
* Check if user has permission to create a form for a specific workflow
* Either the user owns the workflow directly OR has admin permission for the workflow's workspace
*/
export async function checkWorkflowAccessForFormCreation(
workflowId: string,
userId: string
): Promise<{ hasAccess: boolean; workflow?: any }> {
const workflowData = await db.select().from(workflow).where(eq(workflow.id, workflowId)).limit(1)
if (workflowData.length === 0) {
return { hasAccess: false }
}
const workflowRecord = workflowData[0]
if (workflowRecord.userId === userId) {
return { hasAccess: true, workflow: workflowRecord }
}
if (workflowRecord.workspaceId) {
const hasAdmin = await hasAdminPermission(userId, workflowRecord.workspaceId)
if (hasAdmin) {
return { hasAccess: true, workflow: workflowRecord }
}
}
return { hasAccess: false }
}
/**
* Check if user has access to view/edit/delete a specific form
* Either the user owns the form directly OR has admin permission for the workflow's workspace
*/
export async function checkFormAccess(
formId: string,
userId: string
): Promise<{ hasAccess: boolean; form?: any }> {
const formData = await db
.select({
form: form,
workflowWorkspaceId: workflow.workspaceId,
})
.from(form)
.innerJoin(workflow, eq(form.workflowId, workflow.id))
.where(eq(form.id, formId))
.limit(1)
if (formData.length === 0) {
return { hasAccess: false }
}
const { form: formRecord, workflowWorkspaceId } = formData[0]
if (formRecord.userId === userId) {
return { hasAccess: true, form: formRecord }
}
if (workflowWorkspaceId) {
const hasAdmin = await hasAdminPermission(userId, workflowWorkspaceId)
if (hasAdmin) {
return { hasAccess: true, form: formRecord }
}
}
return { hasAccess: false }
}
export async function validateFormAuth(
requestId: string,
deployment: any,
request: NextRequest,
parsedBody?: any
): Promise<{ authorized: boolean; error?: string }> {
const authType = deployment.authType || 'public'
if (authType === 'public') {
return { authorized: true }
}
const cookieName = `form_auth_${deployment.id}`
const authCookie = request.cookies.get(cookieName)
if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) {
return { authorized: true }
}
if (authType === 'password') {
if (request.method === 'GET') {
return { authorized: false, error: 'auth_required_password' }
}
try {
if (!parsedBody) {
return { authorized: false, error: 'Password is required' }
}
const { password, formData } = parsedBody
if (formData && !password) {
return { authorized: false, error: 'auth_required_password' }
}
if (!password) {
return { authorized: false, error: 'Password is required' }
}
if (!deployment.password) {
logger.error(`[${requestId}] No password set for password-protected form: ${deployment.id}`)
return { authorized: false, error: 'Authentication configuration error' }
}
const { decrypted } = await decryptSecret(deployment.password)
if (password !== decrypted) {
return { authorized: false, error: 'Invalid password' }
}
return { authorized: true }
} catch (error) {
logger.error(`[${requestId}] Error validating password:`, error)
return { authorized: false, error: 'Authentication error' }
}
}
if (authType === 'email') {
if (request.method === 'GET') {
return { authorized: false, error: 'auth_required_email' }
}
try {
if (!parsedBody) {
return { authorized: false, error: 'Email is required' }
}
const { email, formData } = parsedBody
if (formData && !email) {
return { authorized: false, error: 'auth_required_email' }
}
if (!email) {
return { authorized: false, error: 'Email is required' }
}
const allowedEmails: string[] = deployment.allowedEmails || []
if (isEmailAllowed(email, allowedEmails)) {
return { authorized: true }
}
return { authorized: false, error: 'Email not authorized for this form' }
} catch (error) {
logger.error(`[${requestId}] Error validating email:`, error)
return { authorized: false, error: 'Authentication error' }
}
}
return { authorized: false, error: 'Unsupported authentication type' }
}
/**
* Form customizations interface
*/
export interface FormCustomizations {
primaryColor?: string
welcomeMessage?: string
thankYouTitle?: string
thankYouMessage?: string
logoUrl?: string
}
/**
* Default form customizations
* Note: primaryColor is intentionally undefined to allow thank you screen to use its green default
*/
export const DEFAULT_FORM_CUSTOMIZATIONS: FormCustomizations = {
welcomeMessage: '',
thankYouTitle: 'Thank you!',
thankYouMessage: 'Your response has been submitted successfully.',
}