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