mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(routes): type all untyped routes (#1848)
* improvement(routes): type all untyped routes * fix routes, remove unused workspace members route * fix obfuscation of errors behind zod errors * remove extraneous comments
This commit is contained in:
@@ -163,6 +163,31 @@ export function SetNewPasswordForm({
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length > 100) {
|
||||
setValidationMessage('Password must not exceed 100 characters')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
setValidationMessage('Password must contain at least one uppercase letter')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
setValidationMessage('Password must contain at least one lowercase letter')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(password)) {
|
||||
setValidationMessage('Password must contain at least one number')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/[^A-Za-z0-9]/.test(password)) {
|
||||
setValidationMessage('Password must contain at least one special character')
|
||||
return
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setValidationMessage('Passwords do not match')
|
||||
return
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('Forget Password API Route', () => {
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Email is required')
|
||||
expect(data.message).toBe('Please provide a valid email address')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -6,15 +7,36 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('ForgetPasswordAPI')
|
||||
|
||||
const forgetPasswordSchema = z.object({
|
||||
email: z
|
||||
.string({ required_error: 'Email is required' })
|
||||
.email('Please provide a valid email address'),
|
||||
redirectTo: z
|
||||
.string()
|
||||
.url('Redirect URL must be a valid URL')
|
||||
.optional()
|
||||
.or(z.literal(''))
|
||||
.transform((val) => (val === '' ? undefined : val)),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { email, redirectTo } = body
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ message: 'Email is required' }, { status: 400 })
|
||||
const validationResult = forgetPasswordSchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
const firstError = validationResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Invalid request data'
|
||||
|
||||
logger.warn('Invalid forget password request data', {
|
||||
errors: validationResult.error.format(),
|
||||
})
|
||||
return NextResponse.json({ message: errorMessage }, { status: 400 })
|
||||
}
|
||||
|
||||
const { email, redirectTo } = validationResult.data
|
||||
|
||||
await auth.api.forgetPassword({
|
||||
body: {
|
||||
email,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { account, user, workflow } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { OAuthService } from '@/lib/oauth/oauth'
|
||||
import { parseProvider } from '@/lib/oauth/oauth'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
@@ -14,6 +14,17 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthCredentialsAPI')
|
||||
|
||||
const credentialsQuerySchema = z
|
||||
.object({
|
||||
provider: z.string().nullish(),
|
||||
workflowId: z.string().uuid('Workflow ID must be a valid UUID').nullish(),
|
||||
credentialId: z.string().uuid('Credential ID must be a valid UUID').nullish(),
|
||||
})
|
||||
.refine((data) => data.provider || data.credentialId, {
|
||||
message: 'Provider or credentialId is required',
|
||||
path: ['provider'],
|
||||
})
|
||||
|
||||
interface GoogleIdToken {
|
||||
email?: string
|
||||
sub?: string
|
||||
@@ -27,11 +38,43 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
// Get query params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const providerParam = searchParams.get('provider') as OAuthService | null
|
||||
const workflowId = searchParams.get('workflowId')
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const rawQuery = {
|
||||
provider: searchParams.get('provider'),
|
||||
workflowId: searchParams.get('workflowId'),
|
||||
credentialId: searchParams.get('credentialId'),
|
||||
}
|
||||
|
||||
const parseResult = credentialsQuerySchema.safeParse(rawQuery)
|
||||
|
||||
if (!parseResult.success) {
|
||||
const refinementError = parseResult.error.errors.find((err) => err.code === 'custom')
|
||||
if (refinementError) {
|
||||
logger.warn(`[${requestId}] Invalid query parameters: ${refinementError.message}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: refinementError.message,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const firstError = parseResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Validation failed'
|
||||
|
||||
logger.warn(`[${requestId}] Invalid query parameters`, {
|
||||
errors: parseResult.error.errors,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: errorMessage,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { provider: providerParam, workflowId, credentialId } = parseResult.data
|
||||
|
||||
// Authenticate requester (supports session, API key, internal JWT)
|
||||
const authResult = await checkHybridAuth(request)
|
||||
@@ -84,11 +127,6 @@ export async function GET(request: NextRequest) {
|
||||
effectiveUserId = requesterUserId
|
||||
}
|
||||
|
||||
if (!providerParam && !credentialId) {
|
||||
logger.warn(`[${requestId}] Missing provider parameter`)
|
||||
return NextResponse.json({ error: 'Provider or credentialId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Parse the provider to get base provider and feature type (if provider is present)
|
||||
const { baseProvider } = parseProvider(providerParam || 'google-default')
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { and, eq, like, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
@@ -10,6 +11,11 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthDisconnectAPI')
|
||||
|
||||
const disconnectSchema = z.object({
|
||||
provider: z.string({ required_error: 'Provider is required' }).min(1, 'Provider is required'),
|
||||
providerId: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Disconnect an OAuth provider for the current user
|
||||
*/
|
||||
@@ -17,23 +23,34 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated disconnect request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get the provider and providerId from the request body
|
||||
const { provider, providerId } = await request.json()
|
||||
const rawBody = await request.json()
|
||||
const parseResult = disconnectSchema.safeParse(rawBody)
|
||||
|
||||
if (!provider) {
|
||||
logger.warn(`[${requestId}] Missing provider in disconnect request`)
|
||||
return NextResponse.json({ error: 'Provider is required' }, { status: 400 })
|
||||
if (!parseResult.success) {
|
||||
const firstError = parseResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Validation failed'
|
||||
|
||||
logger.warn(`[${requestId}] Invalid disconnect request`, {
|
||||
errors: parseResult.error.errors,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: errorMessage,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { provider, providerId } = parseResult.data
|
||||
|
||||
logger.info(`[${requestId}] Processing OAuth disconnect request`, {
|
||||
provider,
|
||||
hasProviderId: !!providerId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -9,6 +10,22 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthTokenAPI')
|
||||
|
||||
const tokenRequestSchema = z.object({
|
||||
credentialId: z
|
||||
.string({ required_error: 'Credential ID is required' })
|
||||
.min(1, 'Credential ID is required'),
|
||||
workflowId: z.string().min(1, 'Workflow ID is required').nullish(),
|
||||
})
|
||||
|
||||
const tokenQuerySchema = z.object({
|
||||
credentialId: z
|
||||
.string({
|
||||
required_error: 'Credential ID is required',
|
||||
invalid_type_error: 'Credential ID is required',
|
||||
})
|
||||
.min(1, 'Credential ID is required'),
|
||||
})
|
||||
|
||||
/**
|
||||
* Get an access token for a specific credential
|
||||
* Supports both session-based authentication (for client-side requests)
|
||||
@@ -20,19 +37,31 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`[${requestId}] OAuth token API POST request received`)
|
||||
|
||||
try {
|
||||
// Parse request body
|
||||
const body = await request.json()
|
||||
const { credentialId, workflowId } = body
|
||||
const rawBody = await request.json()
|
||||
const parseResult = tokenRequestSchema.safeParse(rawBody)
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Credential ID is required`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
if (!parseResult.success) {
|
||||
const firstError = parseResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Validation failed'
|
||||
|
||||
logger.warn(`[${requestId}] Invalid token request`, {
|
||||
errors: parseResult.error.errors,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: errorMessage,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { credentialId, workflowId } = parseResult.data
|
||||
|
||||
// We already have workflowId from the parsed body; avoid forcing hybrid auth to re-read it
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId,
|
||||
workflowId,
|
||||
workflowId: workflowId ?? undefined,
|
||||
requireWorkflowIdForInternal: false,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
@@ -63,15 +92,31 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
// Get the credential ID from the query params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credential ID`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
const rawQuery = {
|
||||
credentialId: searchParams.get('credentialId'),
|
||||
}
|
||||
|
||||
const parseResult = tokenQuerySchema.safeParse(rawQuery)
|
||||
|
||||
if (!parseResult.success) {
|
||||
const firstError = parseResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Validation failed'
|
||||
|
||||
logger.warn(`[${requestId}] Invalid query parameters`, {
|
||||
errors: parseResult.error.errors,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: errorMessage,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { credentialId } = parseResult.data
|
||||
|
||||
// For GET requests, we only support session-based authentication
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Reset Password API Route', () => {
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: 'newSecurePassword123',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
@@ -39,7 +39,7 @@ describe('Reset Password API Route', () => {
|
||||
expect(auth.auth.api.resetPassword).toHaveBeenCalledWith({
|
||||
body: {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: 'newSecurePassword123',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
@@ -58,7 +58,7 @@ describe('Reset Password API Route', () => {
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Token and new password are required')
|
||||
expect(data.message).toBe('Token is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
@@ -77,7 +77,7 @@ describe('Reset Password API Route', () => {
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Token and new password are required')
|
||||
expect(data.message).toBe('Password is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
@@ -97,7 +97,7 @@ describe('Reset Password API Route', () => {
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Token and new password are required')
|
||||
expect(data.message).toBe('Token is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
@@ -117,7 +117,7 @@ describe('Reset Password API Route', () => {
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Token and new password are required')
|
||||
expect(data.message).toBe('Password must be at least 8 characters long')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
@@ -137,7 +137,7 @@ describe('Reset Password API Route', () => {
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'invalid-token',
|
||||
newPassword: 'newSecurePassword123',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
@@ -168,7 +168,7 @@ describe('Reset Password API Route', () => {
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: 'newSecurePassword123',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -6,15 +7,35 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('PasswordResetAPI')
|
||||
|
||||
const resetPasswordSchema = z.object({
|
||||
token: z.string({ required_error: 'Token is required' }).min(1, 'Token is required'),
|
||||
newPassword: z
|
||||
.string({ required_error: 'Password is required' })
|
||||
.min(8, 'Password must be at least 8 characters long')
|
||||
.max(100, 'Password must not exceed 100 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { token, newPassword } = body
|
||||
|
||||
if (!token || !newPassword) {
|
||||
return NextResponse.json({ message: 'Token and new password are required' }, { status: 400 })
|
||||
const validationResult = resetPasswordSchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
const firstError = validationResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Invalid request data'
|
||||
|
||||
logger.warn('Invalid password reset request data', {
|
||||
errors: validationResult.error.format(),
|
||||
})
|
||||
return NextResponse.json({ message: errorMessage }, { status: 400 })
|
||||
}
|
||||
|
||||
const { token, newPassword } = validationResult.data
|
||||
|
||||
await auth.api.resetPassword({
|
||||
body: {
|
||||
newPassword,
|
||||
|
||||
@@ -1,68 +1,93 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('SSO-Register')
|
||||
|
||||
const mappingSchema = z
|
||||
.object({
|
||||
id: z.string().default('sub'),
|
||||
email: z.string().default('email'),
|
||||
name: z.string().default('name'),
|
||||
image: z.string().default('picture'),
|
||||
})
|
||||
.default({
|
||||
id: 'sub',
|
||||
email: 'email',
|
||||
name: 'name',
|
||||
image: 'picture',
|
||||
})
|
||||
|
||||
const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
|
||||
z.object({
|
||||
providerType: z.literal('oidc').default('oidc'),
|
||||
providerId: z.string().min(1, 'Provider ID is required'),
|
||||
issuer: z.string().url('Issuer must be a valid URL'),
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
mapping: mappingSchema,
|
||||
clientId: z.string().min(1, 'Client ID is required for OIDC'),
|
||||
clientSecret: z.string().min(1, 'Client Secret is required for OIDC'),
|
||||
scopes: z
|
||||
.union([
|
||||
z.string().transform((s) =>
|
||||
s
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
),
|
||||
z.array(z.string()),
|
||||
])
|
||||
.default(['openid', 'profile', 'email']),
|
||||
pkce: z.boolean().default(true),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal('saml'),
|
||||
providerId: z.string().min(1, 'Provider ID is required'),
|
||||
issuer: z.string().url('Issuer must be a valid URL'),
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
mapping: mappingSchema,
|
||||
entryPoint: z.string().url('Entry point must be a valid URL for SAML'),
|
||||
cert: z.string().min(1, 'Certificate is required for SAML'),
|
||||
callbackUrl: z.string().url().optional(),
|
||||
audience: z.string().optional(),
|
||||
wantAssertionsSigned: z.boolean().optional(),
|
||||
signatureAlgorithm: z.string().optional(),
|
||||
digestAlgorithm: z.string().optional(),
|
||||
identifierFormat: z.string().optional(),
|
||||
idpMetadata: z.string().optional(),
|
||||
}),
|
||||
])
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!env.SSO_ENABLED) {
|
||||
return NextResponse.json({ error: 'SSO is not enabled' }, { status: 400 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
providerId,
|
||||
issuer,
|
||||
domain,
|
||||
providerType = 'oidc',
|
||||
// OIDC specific fields
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes = ['openid', 'profile', 'email'],
|
||||
pkce = true,
|
||||
// SAML specific fields
|
||||
entryPoint,
|
||||
cert,
|
||||
callbackUrl,
|
||||
audience,
|
||||
wantAssertionsSigned,
|
||||
signatureAlgorithm,
|
||||
digestAlgorithm,
|
||||
identifierFormat,
|
||||
idpMetadata,
|
||||
// Mapping configuration
|
||||
mapping = {
|
||||
id: 'sub',
|
||||
email: 'email',
|
||||
name: 'name',
|
||||
image: 'picture',
|
||||
},
|
||||
} = body
|
||||
const rawBody = await request.json()
|
||||
|
||||
const parseResult = ssoRegistrationSchema.safeParse(rawBody)
|
||||
|
||||
if (!parseResult.success) {
|
||||
const firstError = parseResult.error.errors[0]
|
||||
const errorMessage = firstError?.message || 'Validation failed'
|
||||
|
||||
logger.warn('Invalid SSO registration request', {
|
||||
errors: parseResult.error.errors,
|
||||
})
|
||||
|
||||
if (!providerId || !issuer || !domain) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: providerId, issuer, domain' },
|
||||
{
|
||||
error: errorMessage,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (providerType === 'oidc') {
|
||||
if (!clientId || !clientSecret) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required OIDC fields: clientId, clientSecret' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} else if (providerType === 'saml') {
|
||||
if (!entryPoint || !cert) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required SAML fields: entryPoint, cert' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
const body = parseResult.data
|
||||
const { providerId, issuer, domain, providerType, mapping } = body
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
request.headers.forEach((value, key) => {
|
||||
@@ -77,18 +102,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
if (providerType === 'oidc') {
|
||||
const { clientId, clientSecret, scopes, pkce } = body
|
||||
|
||||
const oidcConfig: any = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes:
|
||||
typeof scopes === 'string'
|
||||
? scopes
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter((s: string) => s !== 'offline_access')
|
||||
: (scopes || ['openid', 'profile', 'email']).filter(
|
||||
(s: string) => s !== 'offline_access'
|
||||
),
|
||||
scopes: Array.isArray(scopes)
|
||||
? scopes.filter((s: string) => s !== 'offline_access')
|
||||
: ['openid', 'profile', 'email'].filter((s: string) => s !== 'offline_access'),
|
||||
pkce: pkce ?? true,
|
||||
}
|
||||
|
||||
@@ -138,6 +159,18 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
providerConfig.oidcConfig = oidcConfig
|
||||
} else if (providerType === 'saml') {
|
||||
const {
|
||||
entryPoint,
|
||||
cert,
|
||||
callbackUrl,
|
||||
audience,
|
||||
wantAssertionsSigned,
|
||||
signatureAlgorithm,
|
||||
digestAlgorithm,
|
||||
identifierFormat,
|
||||
idpMetadata,
|
||||
} = body
|
||||
|
||||
const computedCallbackUrl =
|
||||
callbackUrl || `${issuer.replace('/metadata', '')}/callback/${providerId}`
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await req.json()
|
||||
const validation = UpdateCostSchema.safeParse(body)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { chat, workflow, workspace } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { ChatFiles } from '@/lib/uploads'
|
||||
@@ -17,6 +18,22 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/
|
||||
|
||||
const logger = createLogger('ChatIdentifierAPI')
|
||||
|
||||
const chatFileSchema = z.object({
|
||||
name: z.string().min(1, 'File name is required'),
|
||||
type: z.string().min(1, 'File type is required'),
|
||||
size: z.number().positive('File size must be positive'),
|
||||
data: z.string().min(1, 'File data is required'),
|
||||
lastModified: z.number().optional(),
|
||||
})
|
||||
|
||||
const chatPostBodySchema = z.object({
|
||||
input: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
email: z.string().email('Invalid email format').optional().or(z.literal('')),
|
||||
conversationId: z.string().optional(),
|
||||
files: z.array(chatFileSchema).optional().default([]),
|
||||
})
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
@@ -32,7 +49,21 @@ export async function POST(
|
||||
|
||||
let parsedBody
|
||||
try {
|
||||
parsedBody = await request.json()
|
||||
const rawBody = await request.json()
|
||||
const validation = chatPostBodySchema.safeParse(rawBody)
|
||||
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return addCorsHeaders(
|
||||
createErrorResponse(`Invalid request body: ${errorMessage}`, 400),
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
parsedBody = validation.data
|
||||
} catch (_error) {
|
||||
return addCorsHeaders(createErrorResponse('Invalid request body', 400), request)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ export async function POST(request: NextRequest) {
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json()
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,11 +2,20 @@ import { db } from '@sim/db'
|
||||
import { chat } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
const logger = createLogger('ChatValidateAPI')
|
||||
|
||||
const validateQuerySchema = z.object({
|
||||
identifier: z
|
||||
.string()
|
||||
.min(1, 'Identifier is required')
|
||||
.regex(/^[a-z0-9-]+$/, 'Identifier can only contain lowercase letters, numbers, and hyphens')
|
||||
.max(100, 'Identifier must be 100 characters or less'),
|
||||
})
|
||||
|
||||
/**
|
||||
* GET endpoint to validate chat identifier availability
|
||||
*/
|
||||
@@ -15,27 +24,34 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const identifier = searchParams.get('identifier')
|
||||
|
||||
if (!identifier) {
|
||||
return createErrorResponse('Identifier parameter is required', 400)
|
||||
const validation = validateQuerySchema.safeParse({ identifier })
|
||||
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors[0]?.message || 'Invalid identifier'
|
||||
logger.warn(`Validation error: ${errorMessage}`)
|
||||
|
||||
if (identifier && !/^[a-z0-9-]+$/.test(identifier)) {
|
||||
return createSuccessResponse({
|
||||
available: false,
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
return createErrorResponse(errorMessage, 400)
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9-]+$/.test(identifier)) {
|
||||
return createSuccessResponse({
|
||||
available: false,
|
||||
error: 'Identifier can only contain lowercase letters, numbers, and hyphens',
|
||||
})
|
||||
}
|
||||
const { identifier: validatedIdentifier } = validation.data
|
||||
|
||||
const existingChat = await db
|
||||
.select({ id: chat.id })
|
||||
.from(chat)
|
||||
.where(eq(chat.identifier, identifier))
|
||||
.where(eq(chat.identifier, validatedIdentifier))
|
||||
.limit(1)
|
||||
|
||||
const isAvailable = existingChat.length === 0
|
||||
|
||||
logger.debug(
|
||||
`Identifier "${identifier}" availability check: ${isAvailable ? 'available' : 'taken'}`
|
||||
`Identifier "${validatedIdentifier}" availability check: ${isAvailable ? 'available' : 'taken'}`
|
||||
)
|
||||
|
||||
return createSuccessResponse({
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent/constants'
|
||||
|
||||
const GenerateApiKeySchema = z.object({}).optional()
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
@@ -15,7 +18,18 @@ export async function POST(req: NextRequest) {
|
||||
// Move environment variable access inside the function
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
await req.json().catch(() => ({}))
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const validationResult = GenerateApiKeySchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Invalid request body',
|
||||
details: validationResult.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/generate`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor'
|
||||
import { checkInternalApiKey } from '@/lib/copilot/utils'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('CopilotApiKeysValidate')
|
||||
|
||||
const ValidateApiKeySchema = z.object({
|
||||
userId: z.string().min(1, 'userId is required'),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Authenticate via internal API key header
|
||||
const auth = checkInternalApiKey(req)
|
||||
if (!auth.success) {
|
||||
return new NextResponse(null, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await req.json().catch(() => null)
|
||||
const userId = typeof body?.userId === 'string' ? body.userId : undefined
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'userId is required' }, { status: 400 })
|
||||
const validationResult = ValidateApiKeySchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
logger.warn('Invalid validation request', { errors: validationResult.error.errors })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'userId is required',
|
||||
details: validationResult.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { userId } = validationResult.data
|
||||
|
||||
logger.info('[API VALIDATION] Validating usage limit', { userId })
|
||||
|
||||
const { isExceeded, currentUsage, limit } = await checkServerSideUsageLimits(userId)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -7,6 +8,13 @@ const logger = createLogger('CopilotTrainingExamplesAPI')
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const TrainingExampleSchema = z.object({
|
||||
json: z.string().min(1, 'JSON string is required'),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
tags: z.array(z.string()).optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const baseUrl = env.AGENT_INDEXER_URL
|
||||
if (!baseUrl) {
|
||||
@@ -23,8 +31,24 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const validationResult = TrainingExampleSchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
logger.warn('Invalid training example format', { errors: validationResult.error.errors })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Invalid training example format',
|
||||
details: validationResult.error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data
|
||||
|
||||
logger.info('Sending workflow example to agent indexer', {
|
||||
hasJsonField: typeof body?.json === 'string',
|
||||
hasJsonField: typeof validatedData.json === 'string',
|
||||
title: validatedData.title,
|
||||
})
|
||||
|
||||
const upstream = await fetch(`${baseUrl}/examples/add`, {
|
||||
@@ -33,7 +57,7 @@ export async function POST(request: NextRequest) {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify(validatedData),
|
||||
})
|
||||
|
||||
if (!upstream.ok) {
|
||||
|
||||
@@ -5,18 +5,25 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('CopilotTrainingAPI')
|
||||
|
||||
// Schema for the request body
|
||||
const WorkflowStateSchema = z.record(z.unknown())
|
||||
|
||||
const OperationSchema = z.object({
|
||||
type: z.string(),
|
||||
data: z.record(z.unknown()).optional(),
|
||||
timestamp: z.number().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
const TrainingDataSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
prompt: z.string().min(1),
|
||||
input: z.any(), // Workflow state (start)
|
||||
output: z.any(), // Workflow state (end)
|
||||
operations: z.any(),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
prompt: z.string().min(1, 'Prompt is required'),
|
||||
input: WorkflowStateSchema,
|
||||
output: WorkflowStateSchema,
|
||||
operations: z.array(OperationSchema),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check for required environment variables
|
||||
const baseUrl = env.AGENT_INDEXER_URL
|
||||
if (!baseUrl) {
|
||||
logger.error('Missing AGENT_INDEXER_URL environment variable')
|
||||
@@ -32,7 +39,6 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json()
|
||||
const validationResult = TrainingDataSchema.safeParse(body)
|
||||
|
||||
@@ -51,10 +57,9 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
logger.info('Sending training data to agent indexer', {
|
||||
title,
|
||||
operationsCount: Array.isArray(operations) ? operations.length : 0,
|
||||
operationsCount: operations.length,
|
||||
})
|
||||
|
||||
// Forward to agent indexer
|
||||
const upstreamUrl = `${baseUrl}/operations/add`
|
||||
const upstreamResponse = await fetch(upstreamUrl, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -2,12 +2,20 @@ import { db } from '@sim/db'
|
||||
import { workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('FoldersIDAPI')
|
||||
|
||||
const updateFolderSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
isExpanded: z.boolean().optional(),
|
||||
parentId: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
// PUT - Update a folder
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
@@ -18,7 +26,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { name, color, isExpanded, parentId } = body
|
||||
|
||||
const validationResult = updateFolderSchema.safeParse(body)
|
||||
if (!validationResult.success) {
|
||||
logger.error('Folder update validation failed:', {
|
||||
errors: validationResult.error.errors,
|
||||
})
|
||||
const errorMessages = validationResult.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
return NextResponse.json({ error: `Validation failed: ${errorMessages}` }, { status: 400 })
|
||||
}
|
||||
|
||||
const { name, color, isExpanded, parentId } = validationResult.data
|
||||
|
||||
// Verify the folder exists
|
||||
const existingFolder = await db
|
||||
|
||||
@@ -2,11 +2,32 @@ import { db } from '@sim/db'
|
||||
import { memory } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
|
||||
const logger = createLogger('MemoryByIdAPI')
|
||||
|
||||
const memoryQuerySchema = z.object({
|
||||
workflowId: z.string().uuid('Invalid workflow ID format'),
|
||||
})
|
||||
|
||||
const agentMemoryDataSchema = z.object({
|
||||
role: z.enum(['user', 'assistant', 'system'], {
|
||||
errorMap: () => ({ message: 'Role must be user, assistant, or system' }),
|
||||
}),
|
||||
content: z.string().min(1, 'Content is required'),
|
||||
})
|
||||
|
||||
const genericMemoryDataSchema = z.record(z.unknown())
|
||||
|
||||
const memoryPutBodySchema = z.object({
|
||||
data: z.union([agentMemoryDataSchema, genericMemoryDataSchema], {
|
||||
errorMap: () => ({ message: 'Invalid memory data structure' }),
|
||||
}),
|
||||
workflowId: z.string().uuid('Invalid workflow ID format'),
|
||||
})
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
@@ -24,29 +45,36 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required parameter: workflowId`)
|
||||
const validation = memoryQuerySchema.safeParse({ workflowId })
|
||||
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId parameter is required',
|
||||
message: errorMessage,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workflowId: validatedWorkflowId } = validation.data
|
||||
|
||||
// Query the database for the memory
|
||||
const memories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, workflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.orderBy(memory.createdAt)
|
||||
.limit(1)
|
||||
|
||||
if (memories.length === 0) {
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${workflowId}`)
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${validatedWorkflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -58,7 +86,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Memory retrieved successfully: ${id} for workflow: ${workflowId}`)
|
||||
logger.info(
|
||||
`[${requestId}] Memory retrieved successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
@@ -96,28 +126,35 @@ export async function DELETE(
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required parameter: workflowId`)
|
||||
const validation = memoryQuerySchema.safeParse({ workflowId })
|
||||
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId parameter is required',
|
||||
message: errorMessage,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workflowId: validatedWorkflowId } = validation.data
|
||||
|
||||
// Verify memory exists before attempting to delete
|
||||
const existingMemory = await db
|
||||
.select({ id: memory.id })
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, workflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.limit(1)
|
||||
|
||||
if (existingMemory.length === 0) {
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${workflowId}`)
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${validatedWorkflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -130,9 +167,13 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
// Hard delete the memory
|
||||
await db.delete(memory).where(and(eq(memory.key, id), eq(memory.workflowId, workflowId)))
|
||||
await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
|
||||
logger.info(`[${requestId}] Memory deleted successfully: ${id} for workflow: ${workflowId}`)
|
||||
logger.info(
|
||||
`[${requestId}] Memory deleted successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
@@ -163,30 +204,37 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
try {
|
||||
logger.info(`[${requestId}] Processing memory update request for ID: ${id}`)
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json()
|
||||
const { data, workflowId } = body
|
||||
let validatedData
|
||||
let validatedWorkflowId
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = memoryPutBodySchema.safeParse(body)
|
||||
|
||||
if (!data) {
|
||||
logger.warn(`[${requestId}] Missing required field: data`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory data is required',
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Invalid request body: ${errorMessage}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required field: workflowId`)
|
||||
validatedData = validation.data.data
|
||||
validatedWorkflowId = validation.data.workflowId
|
||||
} catch (error: any) {
|
||||
logger.warn(`[${requestId}] Failed to parse request body: ${error.message}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId is required',
|
||||
message: 'Invalid JSON in request body',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
@@ -197,11 +245,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const existingMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, workflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.limit(1)
|
||||
|
||||
if (existingMemories.length === 0) {
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${workflowId}`)
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${validatedWorkflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -215,28 +263,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const existingMemory = existingMemories[0]
|
||||
|
||||
// Validate memory data based on the existing memory type
|
||||
// Additional validation for agent memory type
|
||||
if (existingMemory.type === 'agent') {
|
||||
if (!data.role || !data.content) {
|
||||
logger.warn(`[${requestId}] Missing agent memory fields`)
|
||||
const agentValidation = agentMemoryDataSchema.safeParse(validatedData)
|
||||
if (!agentValidation.success) {
|
||||
const errorMessage = agentValidation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Agent memory validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Agent memory requires role and content',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['user', 'assistant', 'system'].includes(data.role)) {
|
||||
logger.warn(`[${requestId}] Invalid agent role: ${data.role}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Agent role must be user, assistant, or system',
|
||||
message: `Invalid agent memory data: ${errorMessage}`,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
@@ -245,16 +284,20 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
// Update the memory with new data
|
||||
await db.delete(memory).where(and(eq(memory.key, id), eq(memory.workflowId, workflowId)))
|
||||
await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
|
||||
// Fetch the updated memory
|
||||
const updatedMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, workflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.limit(1)
|
||||
|
||||
logger.info(`[${requestId}] Memory updated successfully: ${id} for workflow: ${workflowId}`)
|
||||
logger.info(
|
||||
`[${requestId}] Memory updated successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
|
||||
@@ -13,12 +13,19 @@ import {
|
||||
} from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { requireStripeClient } from '@/lib/billing/stripe-client'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('OrganizationInvitation')
|
||||
|
||||
const updateInvitationSchema = z.object({
|
||||
status: z.enum(['accepted', 'rejected', 'cancelled'], {
|
||||
errorMap: () => ({ message: 'Invalid status. Must be "accepted", "rejected", or "cancelled"' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Get invitation details
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
@@ -84,15 +91,16 @@ export async function PUT(
|
||||
}
|
||||
|
||||
try {
|
||||
const { status } = await req.json()
|
||||
const body = await req.json()
|
||||
|
||||
if (!status || !['accepted', 'rejected', 'cancelled'].includes(status)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid status. Must be "accepted", "rejected", or "cancelled"' },
|
||||
{ status: 400 }
|
||||
)
|
||||
const validation = updateInvitationSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { status } = validation.data
|
||||
|
||||
const orgInvitation = await db
|
||||
.select()
|
||||
.from(invitation)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
||||
import { member, subscription as subscriptionTable, user, userStats } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getUserUsageData } from '@/lib/billing/core/usage'
|
||||
import { requireStripeClient } from '@/lib/billing/stripe-client'
|
||||
@@ -9,6 +10,12 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('OrganizationMemberAPI')
|
||||
|
||||
const updateMemberSchema = z.object({
|
||||
role: z.enum(['owner', 'admin', 'member'], {
|
||||
errorMap: () => ({ message: 'Invalid role' }),
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/organizations/[id]/members/[memberId]
|
||||
* Get individual organization member details
|
||||
@@ -141,14 +148,16 @@ export async function PUT(
|
||||
}
|
||||
|
||||
const { id: organizationId, memberId } = await params
|
||||
const { role } = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
// Validate input
|
||||
if (!role || !['admin', 'member'].includes(role)) {
|
||||
return NextResponse.json({ error: 'Invalid role' }, { status: 400 })
|
||||
const validation = updateMemberSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify user has admin access
|
||||
const { role } = validation.data
|
||||
|
||||
const userMember = await db
|
||||
.select()
|
||||
.from(member)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
||||
import { member, organization } from '@sim/db/schema'
|
||||
import { and, eq, ne } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import {
|
||||
getOrganizationSeatAnalytics,
|
||||
@@ -12,6 +13,21 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('OrganizationAPI')
|
||||
|
||||
const updateOrganizationSchema = z.object({
|
||||
name: z.string().trim().min(1, 'Organization name is required').optional(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Organization slug is required')
|
||||
.regex(
|
||||
/^[a-z0-9-_]+$/,
|
||||
'Slug can only contain lowercase letters, numbers, hyphens, and underscores'
|
||||
)
|
||||
.optional(),
|
||||
logo: z.string().nullable().optional(),
|
||||
seats: z.number().int().min(1, 'Invalid seat count').optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/organizations/[id]
|
||||
* Get organization details including settings and seat information
|
||||
@@ -112,7 +128,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const { id: organizationId } = await params
|
||||
const body = await request.json()
|
||||
const { name, slug, logo, seats } = body
|
||||
|
||||
const validation = updateOrganizationSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { name, slug, logo, seats } = validation.data
|
||||
|
||||
// Verify user has admin access
|
||||
const memberEntry = await db
|
||||
@@ -134,10 +157,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
// Handle seat count update
|
||||
if (seats !== undefined) {
|
||||
if (typeof seats !== 'number' || seats < 1) {
|
||||
return NextResponse.json({ error: 'Invalid seat count' }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await updateOrganizationSeats(organizationId, seats, session.user.id)
|
||||
|
||||
if (!result.success) {
|
||||
@@ -163,28 +182,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
// Handle settings update
|
||||
if (name !== undefined || slug !== undefined || logo !== undefined) {
|
||||
// Validate required fields
|
||||
if (name !== undefined && (!name || typeof name !== 'string' || name.trim().length === 0)) {
|
||||
return NextResponse.json({ error: 'Organization name is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (slug !== undefined && (!slug || typeof slug !== 'string' || slug.trim().length === 0)) {
|
||||
return NextResponse.json({ error: 'Organization slug is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate slug format
|
||||
// Check if slug is already taken by another organization
|
||||
if (slug !== undefined) {
|
||||
const slugRegex = /^[a-z0-9-_]+$/
|
||||
if (!slugRegex.test(slug)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Slug can only contain lowercase letters, numbers, hyphens, and underscores',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if slug is already taken by another organization
|
||||
const existingSlug = await db
|
||||
.select()
|
||||
.from(organization)
|
||||
@@ -198,9 +197,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updateData: any = { updatedAt: new Date() }
|
||||
if (name !== undefined) updateData.name = name.trim()
|
||||
if (slug !== undefined) updateData.slug = slug.trim()
|
||||
if (logo !== undefined) updateData.logo = logo || null
|
||||
if (name !== undefined) updateData.name = name
|
||||
if (slug !== undefined) updateData.slug = slug
|
||||
if (logo !== undefined) updateData.logo = logo
|
||||
|
||||
// Update organization
|
||||
const updatedOrg = await db
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { isDev } from '@/lib/environment'
|
||||
@@ -12,6 +13,19 @@ import { getTool, validateRequiredParametersAfterMerge } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('ProxyAPI')
|
||||
|
||||
const proxyPostSchema = z.object({
|
||||
toolId: z.string().min(1, 'toolId is required'),
|
||||
params: z.record(z.any()).optional().default({}),
|
||||
executionContext: z
|
||||
.object({
|
||||
workflowId: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a minimal set of default headers for proxy requests
|
||||
* @returns Record of HTTP headers
|
||||
@@ -266,13 +280,19 @@ export async function POST(request: NextRequest) {
|
||||
throw new Error('Invalid JSON in request body')
|
||||
}
|
||||
|
||||
const { toolId, params, executionContext } = requestBody
|
||||
|
||||
if (!toolId) {
|
||||
logger.error(`[${requestId}] Missing toolId in request`)
|
||||
throw new Error('Missing toolId in request')
|
||||
const validationResult = proxyPostSchema.safeParse(requestBody)
|
||||
if (!validationResult.success) {
|
||||
logger.error(`[${requestId}] Request validation failed`, {
|
||||
errors: validationResult.error.errors,
|
||||
})
|
||||
const errorMessages = validationResult.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
throw new Error(`Validation failed: ${errorMessages}`)
|
||||
}
|
||||
|
||||
const { toolId, params } = validationResult.data
|
||||
|
||||
logger.info(`[${requestId}] Processing tool: ${toolId}`)
|
||||
|
||||
const tool = getTool(toolId)
|
||||
@@ -306,17 +326,12 @@ export async function POST(request: NextRequest) {
|
||||
(output) => output.type === 'file' || output.type === 'file[]'
|
||||
)
|
||||
|
||||
// Add userId to execution context for file uploads
|
||||
const contextWithUser = executionContext
|
||||
? { ...executionContext, userId: authResult.userId }
|
||||
: undefined
|
||||
|
||||
const result = await executeTool(
|
||||
toolId,
|
||||
params,
|
||||
true, // skipProxy (we're already in the proxy)
|
||||
!hasFileOutputs, // skipPostProcess (don't skip if tool has file outputs)
|
||||
contextWithUser // pass execution context with userId for file processing
|
||||
undefined // execution context is not available in proxy context
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
||||
import { workflow, workflowSchedule } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
@@ -11,6 +12,18 @@ const logger = createLogger('ScheduleAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const scheduleActionEnum = z.enum(['reactivate', 'disable'])
|
||||
const scheduleStatusEnum = z.enum(['active', 'disabled'])
|
||||
|
||||
const scheduleUpdateSchema = z
|
||||
.object({
|
||||
action: scheduleActionEnum.optional(),
|
||||
status: scheduleStatusEnum.optional(),
|
||||
})
|
||||
.refine((data) => data.action || data.status, {
|
||||
message: 'Either action or status must be provided',
|
||||
})
|
||||
|
||||
/**
|
||||
* Delete a schedule
|
||||
*/
|
||||
@@ -99,7 +112,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { action } = body
|
||||
const validation = scheduleUpdateSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
logger.warn(`[${requestId}] Validation error:`, firstError)
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { action, status: requestedStatus } = validation.data
|
||||
|
||||
const [schedule] = await db
|
||||
.select({
|
||||
@@ -143,7 +164,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'Not authorized to modify this schedule' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (action === 'reactivate' || (body.status && body.status === 'active')) {
|
||||
if (action === 'reactivate' || (requestedStatus && requestedStatus === 'active')) {
|
||||
if (schedule.status === 'active') {
|
||||
return NextResponse.json({ message: 'Schedule is already active' }, { status: 200 })
|
||||
}
|
||||
@@ -169,7 +190,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
if (action === 'disable' || (body.status && body.status === 'disabled')) {
|
||||
if (action === 'disable' || (requestedStatus && requestedStatus === 'disabled')) {
|
||||
if (schedule.status === 'disabled') {
|
||||
return NextResponse.json({ message: 'Schedule is already disabled' }, { status: 200 })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
@@ -7,37 +8,55 @@ const logger = createLogger('ConfluenceCommentAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const putCommentSchema = z
|
||||
.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
commentId: z.string().min(1, 'Comment ID is required'),
|
||||
comment: z.string().min(1, 'Comment is required'),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.commentId, 'commentId', 255)
|
||||
return validation.isValid
|
||||
},
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.commentId, 'commentId', 255)
|
||||
return { message: validation.error || 'Invalid comment ID', path: ['commentId'] }
|
||||
}
|
||||
)
|
||||
|
||||
const deleteCommentSchema = z
|
||||
.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
commentId: z.string().min(1, 'Comment ID is required'),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.commentId, 'commentId', 255)
|
||||
return validation.isValid
|
||||
},
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.commentId, 'commentId', 255)
|
||||
return { message: validation.error || 'Invalid comment ID', path: ['commentId'] }
|
||||
}
|
||||
)
|
||||
|
||||
// Update a comment
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
commentId,
|
||||
comment,
|
||||
} = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
const validation = putCommentSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!commentId) {
|
||||
return NextResponse.json({ error: 'Comment ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!comment) {
|
||||
return NextResponse.json({ error: 'Comment is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const commentIdValidation = validateAlphanumericId(commentId, 'commentId', 255)
|
||||
if (!commentIdValidation.isValid) {
|
||||
return NextResponse.json({ error: commentIdValidation.error }, { status: 400 })
|
||||
}
|
||||
const { domain, accessToken, cloudId: providedCloudId, commentId, comment } = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
@@ -64,7 +83,7 @@ export async function PUT(request: Request) {
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}`
|
||||
|
||||
const body = {
|
||||
const updateBody = {
|
||||
body: {
|
||||
representation: 'storage',
|
||||
value: comment,
|
||||
@@ -82,7 +101,7 @@ export async function PUT(request: Request) {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify(updateBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -111,24 +130,15 @@ export async function PUT(request: Request) {
|
||||
// Delete a comment
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const { domain, accessToken, cloudId: providedCloudId, commentId } = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
const validation = deleteCommentSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!commentId) {
|
||||
return NextResponse.json({ error: 'Comment ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const commentIdValidation = validateAlphanumericId(commentId, 'commentId', 255)
|
||||
if (!commentIdValidation.isValid) {
|
||||
return NextResponse.json({ error: commentIdValidation.error }, { status: 400 })
|
||||
}
|
||||
const { domain, accessToken, cloudId: providedCloudId, commentId } = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
@@ -7,26 +8,82 @@ const logger = createLogger('ConfluencePageAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const postPageSchema = z
|
||||
.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
return validation.isValid
|
||||
},
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
return { message: validation.error || 'Invalid page ID', path: ['pageId'] }
|
||||
}
|
||||
)
|
||||
|
||||
const putPageSchema = z
|
||||
.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
title: z.string().optional(),
|
||||
body: z
|
||||
.object({
|
||||
value: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
version: z
|
||||
.object({
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
return validation.isValid
|
||||
},
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
return { message: validation.error || 'Invalid page ID', path: ['pageId'] }
|
||||
}
|
||||
)
|
||||
|
||||
const deletePageSchema = z
|
||||
.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
return validation.isValid
|
||||
},
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
return { message: validation.error || 'Invalid page ID', path: ['pageId'] }
|
||||
}
|
||||
)
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { domain, accessToken, pageId, cloudId: providedCloudId } = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
const validation = postPageSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!pageId) {
|
||||
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||
if (!pageIdValidation.isValid) {
|
||||
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||
}
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId } = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
@@ -91,6 +148,12 @@ export async function PUT(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const validation = putPageSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
@@ -99,24 +162,7 @@ export async function PUT(request: Request) {
|
||||
title,
|
||||
body: pageBody,
|
||||
version,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!pageId) {
|
||||
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||
if (!pageIdValidation.isValid) {
|
||||
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||
}
|
||||
} = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
@@ -204,24 +250,15 @@ export async function PUT(request: Request) {
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const { domain, accessToken, pageId, cloudId: providedCloudId } = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
const validation = deletePageSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!pageId) {
|
||||
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||
if (!pageIdValidation.isValid) {
|
||||
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||
}
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId } = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
@@ -7,8 +8,30 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JiraUpdateAPI')
|
||||
|
||||
const jiraUpdateSchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
issueKey: z.string().min(1, 'Issue key is required'),
|
||||
summary: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
priority: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = jiraUpdateSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
logger.error('Validation error:', firstError)
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
@@ -20,22 +43,7 @@ export async function PUT(request: Request) {
|
||||
priority,
|
||||
assignee,
|
||||
cloudId: providedCloudId,
|
||||
} = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueKey) {
|
||||
logger.error('Missing issue key in request')
|
||||
return NextResponse.json({ error: 'Issue key is required' }, { status: 400 })
|
||||
}
|
||||
} = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
logger.info('Using cloud ID:', cloudId)
|
||||
@@ -97,7 +105,7 @@ export async function PUT(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
const body = { fields }
|
||||
const requestBody = { fields }
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
@@ -106,7 +114,7 @@ export async function PUT(request: Request) {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -958,7 +958,6 @@ The system will substitute actual values when these placeholders are used, keepi
|
||||
try {
|
||||
logger.info('Attempting to extract structured data using Stagehand extract')
|
||||
const schemaObj = getSchemaObject(outputSchema)
|
||||
// Use ensureZodObject to get a proper ZodObject instance
|
||||
const zodSchema = ensureZodObject(logger, schemaObj)
|
||||
|
||||
// Use the extract API to get structured data from whatever page we ended up on
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
import type { Logger } from '@/lib/logs/console/logger'
|
||||
|
||||
// Convert JSON schema to Zod schema (reused from extract route)
|
||||
function jsonSchemaToZod(logger: Logger, jsonSchema: Record<string, any>): z.ZodTypeAny {
|
||||
if (!jsonSchema) {
|
||||
logger.error('Invalid schema: Schema is null or undefined')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getUserUsageLimitInfo, updateUserUsageLimit } from '@/lib/billing'
|
||||
import {
|
||||
@@ -9,6 +10,18 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('UnifiedUsageAPI')
|
||||
|
||||
const usageContextEnum = z.enum(['user', 'organization'])
|
||||
|
||||
const usageUpdateSchema = z
|
||||
.object({
|
||||
limit: z.number().min(0, 'Limit must be a positive number'),
|
||||
context: usageContextEnum.optional().default('user'),
|
||||
organizationId: z.string().optional(),
|
||||
})
|
||||
.refine((data) => data.context !== 'organization' || data.organizationId, {
|
||||
message: 'Organization ID is required when context is organization',
|
||||
})
|
||||
|
||||
/**
|
||||
* Unified Usage Endpoint
|
||||
* GET/PUT /api/usage?context=user|organization&userId=<id>&organizationId=<id>
|
||||
@@ -86,48 +99,34 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const limit = body?.limit
|
||||
const context = body?.context || 'user'
|
||||
const organizationId = body?.organizationId
|
||||
const validation = usageUpdateSchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
logger.error('Validation error:', firstError)
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { limit, context, organizationId } = validation.data
|
||||
const userId = session.user.id
|
||||
|
||||
if (typeof limit !== 'number' || limit < 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid limit. Must be a positive number' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['user', 'organization'].includes(context)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid context. Must be "user" or "organization"' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (context === 'user') {
|
||||
await updateUserUsageLimit(userId, limit)
|
||||
} else if (context === 'organization') {
|
||||
if (!organizationId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Organization ID is required when context=organization' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const hasPermission = await isOrganizationOwnerOrAdmin(session.user.id, organizationId)
|
||||
// organizationId is guaranteed to exist by Zod refinement
|
||||
const hasPermission = await isOrganizationOwnerOrAdmin(session.user.id, organizationId!)
|
||||
if (!hasPermission) {
|
||||
return NextResponse.json({ error: 'Permission denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { updateOrganizationUsageLimit } = await import('@/lib/billing/core/organization')
|
||||
const result = await updateOrganizationUsageLimit(organizationId, limit)
|
||||
const result = await updateOrganizationUsageLimit(organizationId!, limit)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const updated = await getOrganizationBillingData(organizationId)
|
||||
const updated = await getOrganizationBillingData(organizationId!)
|
||||
return NextResponse.json({ success: true, context, userId, organizationId, data: updated })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db, workflowDeploymentVersion } from '@sim/db'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
@@ -8,6 +9,14 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/
|
||||
|
||||
const logger = createLogger('WorkflowDeploymentVersionAPI')
|
||||
|
||||
const patchBodySchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Name cannot be empty')
|
||||
.max(100, 'Name must be 100 characters or less'),
|
||||
})
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
@@ -73,24 +82,17 @@ export async function PATCH(
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { name } = body
|
||||
const validation = patchBodySchema.safeParse(body)
|
||||
|
||||
if (typeof name !== 'string') {
|
||||
return createErrorResponse('Name must be a string', 400)
|
||||
if (!validation.success) {
|
||||
return createErrorResponse(validation.error.errors[0]?.message || 'Invalid request body', 400)
|
||||
}
|
||||
|
||||
const trimmedName = name.trim()
|
||||
if (trimmedName.length === 0) {
|
||||
return createErrorResponse('Name cannot be empty', 400)
|
||||
}
|
||||
|
||||
if (trimmedName.length > 100) {
|
||||
return createErrorResponse('Name must be 100 characters or less', 400)
|
||||
}
|
||||
const { name } = validation.data
|
||||
|
||||
const [updated] = await db
|
||||
.update(workflowDeploymentVersion)
|
||||
.set({ name: trimmedName })
|
||||
.set({ name })
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
@@ -104,7 +106,7 @@ export async function PATCH(
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Renamed deployment version ${version} for workflow ${id} to "${trimmedName}"`
|
||||
`[${requestId}] Renamed deployment version ${version} for workflow ${id} to "${name}"`
|
||||
)
|
||||
|
||||
return createSuccessResponse({ name: updated.name })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkServerSideUsageLimits } from '@/lib/billing'
|
||||
import { processInputFileFields } from '@/lib/execution/files'
|
||||
@@ -22,6 +23,14 @@ import type { SubflowType } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('WorkflowExecuteAPI')
|
||||
|
||||
const ExecuteWorkflowSchema = z.object({
|
||||
selectedOutputs: z.array(z.string()).optional().default([]),
|
||||
triggerType: z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']).optional(),
|
||||
stream: z.boolean().optional(),
|
||||
useDraftState: z.boolean().optional(),
|
||||
input: z.any().optional(),
|
||||
})
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -277,16 +286,40 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
logger.warn(`[${requestId}] Failed to parse request body, using defaults`)
|
||||
}
|
||||
|
||||
const validation = ExecuteWorkflowSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
logger.warn(`[${requestId}] Invalid request body:`, validation.error.errors)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Invalid request body',
|
||||
details: validation.error.errors.map((e) => ({
|
||||
path: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const defaultTriggerType = auth.authType === 'api_key' ? 'api' : 'manual'
|
||||
|
||||
const {
|
||||
selectedOutputs = [],
|
||||
selectedOutputs,
|
||||
triggerType = defaultTriggerType,
|
||||
stream: streamParam,
|
||||
useDraftState,
|
||||
} = body
|
||||
input: validatedInput,
|
||||
} = validation.data
|
||||
|
||||
const input = auth.authType === 'api_key' ? body : body.input
|
||||
// For API key auth, the entire body is the input (except for our control fields)
|
||||
// For session auth, the input is explicitly provided in the input field
|
||||
const input =
|
||||
auth.authType === 'api_key'
|
||||
? (() => {
|
||||
const { selectedOutputs, triggerType, stream, useDraftState, ...rest } = body
|
||||
return Object.keys(rest).length > 0 ? rest : validatedInput
|
||||
})()
|
||||
: validatedInput
|
||||
|
||||
const shouldUseDraftState = useDraftState ?? auth.authType === 'session'
|
||||
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import type { ExecutionResult } from '@/executor/types'
|
||||
|
||||
const logger = createLogger('WorkflowLogAPI')
|
||||
|
||||
const postBodySchema = z.object({
|
||||
logs: z.array(z.any()).optional(),
|
||||
executionId: z.string().min(1, 'Execution ID is required').optional(),
|
||||
result: z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
output: z.any(),
|
||||
metadata: z
|
||||
.object({
|
||||
source: z.string().optional(),
|
||||
duration: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -15,16 +35,30 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
if (validation.error) {
|
||||
logger.warn(`[${requestId}] Workflow access validation failed: ${validation.error.message}`)
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
const accessValidation = await validateWorkflowAccess(request, id, false)
|
||||
if (accessValidation.error) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workflow access validation failed: ${accessValidation.error.message}`
|
||||
)
|
||||
return createErrorResponse(accessValidation.error.message, accessValidation.error.status)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { logs, executionId, result } = body
|
||||
const validation = postBodySchema.safeParse(body)
|
||||
|
||||
if (!validation.success) {
|
||||
logger.warn(`[${requestId}] Invalid request body: ${validation.error.message}`)
|
||||
return createErrorResponse(validation.error.errors[0]?.message || 'Invalid request body', 400)
|
||||
}
|
||||
|
||||
const { logs, executionId, result } = validation.data
|
||||
|
||||
if (result) {
|
||||
if (!executionId) {
|
||||
logger.warn(`[${requestId}] Missing executionId for result logging`)
|
||||
return createErrorResponse('executionId is required when logging results', 400)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Persisting execution result for workflow: ${id}`, {
|
||||
executionId,
|
||||
success: result.success,
|
||||
@@ -35,8 +69,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const triggerType = isChatExecution ? 'chat' : 'manual'
|
||||
const loggingSession = new LoggingSession(id, executionId, triggerType, requestId)
|
||||
|
||||
const userId = validation.workflow.userId
|
||||
const workspaceId = validation.workflow.workspaceId || ''
|
||||
const userId = accessValidation.workflow.userId
|
||||
const workspaceId = accessValidation.workflow.workspaceId || ''
|
||||
|
||||
await loggingSession.safeStart({
|
||||
userId,
|
||||
@@ -44,7 +78,12 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
variables: {},
|
||||
})
|
||||
|
||||
const { traceSpans, totalDuration } = buildTraceSpans(result)
|
||||
const resultWithOutput = {
|
||||
...result,
|
||||
output: result.output ?? {},
|
||||
}
|
||||
|
||||
const { traceSpans, totalDuration } = buildTraceSpans(resultWithOutput as ExecutionResult)
|
||||
|
||||
if (result.success === false) {
|
||||
const message = result.error || 'Workflow execution failed'
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
|
||||
const queryParamsSchema = z.object({
|
||||
status: z.string().optional(),
|
||||
})
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -20,7 +25,18 @@ export async function GET(
|
||||
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
|
||||
}
|
||||
|
||||
const statusFilter = request.nextUrl.searchParams.get('status') || undefined
|
||||
const validation = queryParamsSchema.safeParse({
|
||||
status: request.nextUrl.searchParams.get('status'),
|
||||
})
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{ error: validation.error.errors[0]?.message || 'Invalid query parameters' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { status: statusFilter } = validation.data
|
||||
|
||||
const pausedExecutions = await PauseResumeManager.listPausedExecutions({
|
||||
workflowId,
|
||||
|
||||
@@ -354,7 +354,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json()
|
||||
const updates = UpdateWorkflowSchema.parse(body)
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json()
|
||||
const state = WorkflowStateSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,32 +2,44 @@ import { db } from '@sim/db'
|
||||
import { userStats, workflow } from '@sim/db/schema'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('WorkflowStatsAPI')
|
||||
|
||||
const queryParamsSchema = z.object({
|
||||
runs: z.coerce.number().int().min(1).max(100).default(1),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const runs = Number.parseInt(searchParams.get('runs') || '1', 10)
|
||||
|
||||
if (Number.isNaN(runs) || runs < 1 || runs > 100) {
|
||||
logger.error(`Invalid number of runs: ${runs}`)
|
||||
const validation = queryParamsSchema.safeParse({
|
||||
runs: searchParams.get('runs'),
|
||||
})
|
||||
|
||||
if (!validation.success) {
|
||||
logger.error(`Invalid query parameters: ${validation.error.message}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid number of runs. Must be between 1 and 100.' },
|
||||
{
|
||||
error:
|
||||
validation.error.errors[0]?.message ||
|
||||
'Invalid number of runs. Must be between 1 and 100.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { runs } = validation.data
|
||||
|
||||
try {
|
||||
// Get workflow record
|
||||
const [workflowRecord] = await db.select().from(workflow).where(eq(workflow.id, id)).limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return NextResponse.json({ error: `Workflow ${id} not found` }, { status: 404 })
|
||||
}
|
||||
|
||||
// Update workflow runCount
|
||||
try {
|
||||
await db
|
||||
.update(workflow)
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import crypto from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, type permissionTypeEnum, workspace } from '@sim/db/schema'
|
||||
import { permissions, workspace } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUsersWithPermissions, hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspacesPermissionsAPI')
|
||||
|
||||
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
|
||||
|
||||
interface UpdatePermissionsRequest {
|
||||
updates: Array<{
|
||||
userId: string
|
||||
permissions: PermissionType
|
||||
}>
|
||||
}
|
||||
const updatePermissionsSchema = z.object({
|
||||
updates: z.array(
|
||||
z.object({
|
||||
userId: z.string().uuid(),
|
||||
permissions: z.enum(['admin', 'write', 'read']),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/workspaces/[id]/permissions
|
||||
@@ -92,7 +93,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
)
|
||||
}
|
||||
|
||||
const body: UpdatePermissionsRequest = await request.json()
|
||||
const body = updatePermissionsSchema.parse(await request.json())
|
||||
|
||||
const workspaceRow = await db
|
||||
.select({ billedAccountUserId: workspace.billedAccountUserId })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { workflow } from '@sim/db/schema'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -10,6 +11,16 @@ import { db } from '@sim/db'
|
||||
import { knowledgeBase, permissions, templates, workspace } from '@sim/db/schema'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
|
||||
const patchWorkspaceSchema = z.object({
|
||||
name: z.string().trim().min(1).optional(),
|
||||
billedAccountUserId: z.string().uuid().optional(),
|
||||
allowPersonalApiKeys: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const deleteWorkspaceSchema = z.object({
|
||||
deleteTemplates: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const session = await getSession()
|
||||
@@ -100,16 +111,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
name,
|
||||
billedAccountUserId,
|
||||
allowPersonalApiKeys,
|
||||
}: {
|
||||
name?: string
|
||||
billedAccountUserId?: string
|
||||
allowPersonalApiKeys?: boolean
|
||||
} = body ?? {}
|
||||
const body = patchWorkspaceSchema.parse(await request.json())
|
||||
const { name, billedAccountUserId, allowPersonalApiKeys } = body
|
||||
|
||||
if (
|
||||
name === undefined &&
|
||||
@@ -132,11 +135,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
const updateData: Record<string, unknown> = {}
|
||||
|
||||
if (name !== undefined) {
|
||||
const trimmedName = name.trim()
|
||||
if (!trimmedName) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 })
|
||||
}
|
||||
updateData.name = trimmedName
|
||||
updateData.name = name
|
||||
}
|
||||
|
||||
if (allowPersonalApiKeys !== undefined) {
|
||||
@@ -144,11 +143,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
}
|
||||
|
||||
if (billedAccountUserId !== undefined) {
|
||||
const candidateId = billedAccountUserId?.trim()
|
||||
|
||||
if (!candidateId) {
|
||||
return NextResponse.json({ error: 'billedAccountUserId is required' }, { status: 400 })
|
||||
}
|
||||
const candidateId = billedAccountUserId
|
||||
|
||||
const isOwner = candidateId === existingWorkspace.ownerId
|
||||
|
||||
@@ -219,8 +214,8 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
const workspaceId = id
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const { deleteTemplates = false } = body // User's choice: false = keep templates (recommended), true = delete templates
|
||||
const body = deleteWorkspaceSchema.parse(await request.json().catch(() => ({})))
|
||||
const { deleteTemplates } = body // User's choice: false = keep templates (recommended), true = delete templates
|
||||
|
||||
// Check if user has admin permissions to delete workspace
|
||||
const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
|
||||
|
||||
@@ -2,11 +2,15 @@ import { db } from '@sim/db'
|
||||
import { permissions, workspace } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspaceMemberAPI')
|
||||
const deleteMemberSchema = z.object({
|
||||
workspaceId: z.string().uuid(),
|
||||
})
|
||||
|
||||
// DELETE /api/workspaces/members/[id] - Remove a member from a workspace
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -19,12 +23,8 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
|
||||
try {
|
||||
// Get the workspace ID from the request body or URL
|
||||
const body = await req.json()
|
||||
const workspaceId = body.workspaceId
|
||||
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: 'Workspace ID is required' }, { status: 400 })
|
||||
}
|
||||
const body = deleteMemberSchema.parse(await req.json())
|
||||
const { workspaceId } = body
|
||||
|
||||
const workspaceRow = await db
|
||||
.select({ billedAccountUserId: workspace.billedAccountUserId })
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, type permissionTypeEnum, user } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { hasAdminPermission } from '@/lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkspaceMemberAPI')
|
||||
|
||||
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
|
||||
|
||||
// Add a member to a workspace
|
||||
export async function POST(req: Request) {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { workspaceId, userEmail, permission = 'read' } = await req.json()
|
||||
|
||||
if (!workspaceId || !userEmail) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace ID and user email are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate permission type
|
||||
const validPermissions: PermissionType[] = ['admin', 'write', 'read']
|
||||
if (!validPermissions.includes(permission)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Invalid permission: must be one of ${validPermissions.join(', ')}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if current user has admin permission for the workspace
|
||||
const hasAdmin = await hasAdminPermission(session.user.id, workspaceId)
|
||||
|
||||
if (!hasAdmin) {
|
||||
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const targetUser = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.email, userEmail))
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!targetUser) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if user already has permissions for this workspace
|
||||
const existingPermissions = await db
|
||||
.select()
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, targetUser.id),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
|
||||
if (existingPermissions.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User already has permissions for this workspace' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create single permission for the new member
|
||||
await db.insert(permissions).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: targetUser.id,
|
||||
entityType: 'workspace' as const,
|
||||
entityId: workspaceId,
|
||||
permissionType: permission,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `User added to workspace with ${permission} permission`,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error adding workspace member:', error)
|
||||
return NextResponse.json({ error: 'Failed to add workspace member' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
|
||||
import { permissions, workflow, workspace } from '@sim/db/schema'
|
||||
import { and, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
@@ -9,6 +10,10 @@ import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||
|
||||
const logger = createLogger('Workspaces')
|
||||
|
||||
const createWorkspaceSchema = z.object({
|
||||
name: z.string().trim().min(1, 'Name is required'),
|
||||
})
|
||||
|
||||
// Get all workspaces for the current user
|
||||
export async function GET() {
|
||||
const session = await getSession()
|
||||
@@ -62,11 +67,7 @@ export async function POST(req: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 })
|
||||
}
|
||||
const { name } = createWorkspaceSchema.parse(await req.json())
|
||||
|
||||
const newWorkspace = await createWorkspace(session.user.id, name)
|
||||
|
||||
|
||||
@@ -354,7 +354,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
dataUrl: file.dataUrl || (await fileToBase64(file.file)),
|
||||
data: file.dataUrl || (await fileToBase64(file.file)),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
-1
@@ -91,7 +91,6 @@ export function useChatDeployment() {
|
||||
deployApiEnabled: !existingChatId,
|
||||
}
|
||||
|
||||
// Validate with Zod
|
||||
chatSchema.parse(payload)
|
||||
|
||||
// Determine endpoint and method
|
||||
|
||||
+5
-2
@@ -313,8 +313,11 @@ export function TrainingModal() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
json: JSON.stringify(sanitizedWorkflow),
|
||||
source_path: liveWorkflowTitle,
|
||||
summary: liveWorkflowDescription,
|
||||
title: liveWorkflowTitle,
|
||||
tags: [],
|
||||
metadata: {
|
||||
summary: liveWorkflowDescription,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
+2
-2
@@ -191,7 +191,7 @@ export function CreateMenu({ onCreateWorkflow, isCreatingWorkflow = false }: Cre
|
||||
targetFolderId = folderMap.get(folderPathKey)!
|
||||
}
|
||||
|
||||
const workflowName = extractWorkflowName(workflow.content)
|
||||
const workflowName = extractWorkflowName(workflow.content, workflow.name)
|
||||
const { clearDiff } = useWorkflowDiffStore.getState()
|
||||
clearDiff()
|
||||
|
||||
@@ -252,7 +252,7 @@ export function CreateMenu({ onCreateWorkflow, isCreatingWorkflow = false }: Cre
|
||||
continue
|
||||
}
|
||||
|
||||
const workflowName = extractWorkflowName(workflow.content)
|
||||
const workflowName = extractWorkflowName(workflow.content, workflow.name)
|
||||
const { clearDiff } = useWorkflowDiffStore.getState()
|
||||
clearDiff()
|
||||
|
||||
|
||||
@@ -180,13 +180,26 @@ export async function extractWorkflowsFromFiles(files: File[]): Promise<Imported
|
||||
return workflows
|
||||
}
|
||||
|
||||
export function extractWorkflowName(content: string): string {
|
||||
export function extractWorkflowName(content: string, filename: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
|
||||
if (parsed.state?.metadata?.name && typeof parsed.state.metadata.name === 'string') {
|
||||
return parsed.state.metadata.name.trim()
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// JSON parse failed, fall through to filename
|
||||
}
|
||||
|
||||
return `Imported Workflow ${new Date().toLocaleString()}`
|
||||
let name = filename.replace(/\.json$/i, '')
|
||||
|
||||
name = name.replace(/-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, '')
|
||||
|
||||
name = name
|
||||
.replace(/[-_]/g, ' ')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ')
|
||||
|
||||
return name.trim() || 'Imported Workflow'
|
||||
}
|
||||
|
||||
@@ -19,11 +19,10 @@ const createMockExecutionContext = (overrides?: Partial<ExecutionContext>): Exec
|
||||
workspaceId: 'workspace-456',
|
||||
blockStates: new Map(),
|
||||
blockLogs: [],
|
||||
metadata: { startTime: new Date().toISOString(), duration: 0 },
|
||||
metadata: { duration: 0 },
|
||||
environmentVariables: {},
|
||||
decisions: { router: new Map(), condition: new Map() },
|
||||
loopIterations: new Map(),
|
||||
loopItems: new Map(),
|
||||
loopExecutions: new Map(),
|
||||
completedLoops: new Set(),
|
||||
executedBlocks: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
|
||||
Reference in New Issue
Block a user