Skip to content

Commit bb7016a

Browse files
authored
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
1 parent c427826 commit bb7016a

File tree

48 files changed

+1042
-557
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1042
-557
lines changed

apps/sim/app/(auth)/reset-password/reset-password-form.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,31 @@ export function SetNewPasswordForm({
163163
return
164164
}
165165

166+
if (password.length > 100) {
167+
setValidationMessage('Password must not exceed 100 characters')
168+
return
169+
}
170+
171+
if (!/[A-Z]/.test(password)) {
172+
setValidationMessage('Password must contain at least one uppercase letter')
173+
return
174+
}
175+
176+
if (!/[a-z]/.test(password)) {
177+
setValidationMessage('Password must contain at least one lowercase letter')
178+
return
179+
}
180+
181+
if (!/[0-9]/.test(password)) {
182+
setValidationMessage('Password must contain at least one number')
183+
return
184+
}
185+
186+
if (!/[^A-Za-z0-9]/.test(password)) {
187+
setValidationMessage('Password must contain at least one special character')
188+
return
189+
}
190+
166191
if (password !== confirmPassword) {
167192
setValidationMessage('Passwords do not match')
168193
return

apps/sim/app/api/auth/forget-password/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe('Forget Password API Route', () => {
104104
const data = await response.json()
105105

106106
expect(response.status).toBe(400)
107-
expect(data.message).toBe('Email is required')
107+
expect(data.message).toBe('Please provide a valid email address')
108108

109109
const auth = await import('@/lib/auth')
110110
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()

apps/sim/app/api/auth/forget-password/route.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
23
import { auth } from '@/lib/auth'
34
import { createLogger } from '@/lib/logs/console/logger'
45

56
export const dynamic = 'force-dynamic'
67

78
const logger = createLogger('ForgetPasswordAPI')
89

10+
const forgetPasswordSchema = z.object({
11+
email: z
12+
.string({ required_error: 'Email is required' })
13+
.email('Please provide a valid email address'),
14+
redirectTo: z
15+
.string()
16+
.url('Redirect URL must be a valid URL')
17+
.optional()
18+
.or(z.literal(''))
19+
.transform((val) => (val === '' ? undefined : val)),
20+
})
21+
922
export async function POST(request: NextRequest) {
1023
try {
1124
const body = await request.json()
12-
const { email, redirectTo } = body
1325

14-
if (!email) {
15-
return NextResponse.json({ message: 'Email is required' }, { status: 400 })
26+
const validationResult = forgetPasswordSchema.safeParse(body)
27+
28+
if (!validationResult.success) {
29+
const firstError = validationResult.error.errors[0]
30+
const errorMessage = firstError?.message || 'Invalid request data'
31+
32+
logger.warn('Invalid forget password request data', {
33+
errors: validationResult.error.format(),
34+
})
35+
return NextResponse.json({ message: errorMessage }, { status: 400 })
1636
}
1737

38+
const { email, redirectTo } = validationResult.data
39+
1840
await auth.api.forgetPassword({
1941
body: {
2042
email,

apps/sim/app/api/auth/oauth/credentials/route.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { account, user, workflow } from '@sim/db/schema'
33
import { and, eq } from 'drizzle-orm'
44
import { jwtDecode } from 'jwt-decode'
55
import { type NextRequest, NextResponse } from 'next/server'
6+
import { z } from 'zod'
67
import { checkHybridAuth } from '@/lib/auth/hybrid'
78
import { createLogger } from '@/lib/logs/console/logger'
8-
import type { OAuthService } from '@/lib/oauth/oauth'
99
import { parseProvider } from '@/lib/oauth/oauth'
1010
import { getUserEntityPermissions } from '@/lib/permissions/utils'
1111
import { generateRequestId } from '@/lib/utils'
@@ -14,6 +14,17 @@ export const dynamic = 'force-dynamic'
1414

1515
const logger = createLogger('OAuthCredentialsAPI')
1616

17+
const credentialsQuerySchema = z
18+
.object({
19+
provider: z.string().nullish(),
20+
workflowId: z.string().uuid('Workflow ID must be a valid UUID').nullish(),
21+
credentialId: z.string().uuid('Credential ID must be a valid UUID').nullish(),
22+
})
23+
.refine((data) => data.provider || data.credentialId, {
24+
message: 'Provider or credentialId is required',
25+
path: ['provider'],
26+
})
27+
1728
interface GoogleIdToken {
1829
email?: string
1930
sub?: string
@@ -27,11 +38,43 @@ export async function GET(request: NextRequest) {
2738
const requestId = generateRequestId()
2839

2940
try {
30-
// Get query params
3141
const { searchParams } = new URL(request.url)
32-
const providerParam = searchParams.get('provider') as OAuthService | null
33-
const workflowId = searchParams.get('workflowId')
34-
const credentialId = searchParams.get('credentialId')
42+
const rawQuery = {
43+
provider: searchParams.get('provider'),
44+
workflowId: searchParams.get('workflowId'),
45+
credentialId: searchParams.get('credentialId'),
46+
}
47+
48+
const parseResult = credentialsQuerySchema.safeParse(rawQuery)
49+
50+
if (!parseResult.success) {
51+
const refinementError = parseResult.error.errors.find((err) => err.code === 'custom')
52+
if (refinementError) {
53+
logger.warn(`[${requestId}] Invalid query parameters: ${refinementError.message}`)
54+
return NextResponse.json(
55+
{
56+
error: refinementError.message,
57+
},
58+
{ status: 400 }
59+
)
60+
}
61+
62+
const firstError = parseResult.error.errors[0]
63+
const errorMessage = firstError?.message || 'Validation failed'
64+
65+
logger.warn(`[${requestId}] Invalid query parameters`, {
66+
errors: parseResult.error.errors,
67+
})
68+
69+
return NextResponse.json(
70+
{
71+
error: errorMessage,
72+
},
73+
{ status: 400 }
74+
)
75+
}
76+
77+
const { provider: providerParam, workflowId, credentialId } = parseResult.data
3578

3679
// Authenticate requester (supports session, API key, internal JWT)
3780
const authResult = await checkHybridAuth(request)
@@ -84,11 +127,6 @@ export async function GET(request: NextRequest) {
84127
effectiveUserId = requesterUserId
85128
}
86129

87-
if (!providerParam && !credentialId) {
88-
logger.warn(`[${requestId}] Missing provider parameter`)
89-
return NextResponse.json({ error: 'Provider or credentialId is required' }, { status: 400 })
90-
}
91-
92130
// Parse the provider to get base provider and feature type (if provider is present)
93131
const { baseProvider } = parseProvider(providerParam || 'google-default')
94132

apps/sim/app/api/auth/oauth/disconnect/route.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
22
import { account } from '@sim/db/schema'
33
import { and, eq, like, or } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
5+
import { z } from 'zod'
56
import { getSession } from '@/lib/auth'
67
import { createLogger } from '@/lib/logs/console/logger'
78
import { generateRequestId } from '@/lib/utils'
@@ -10,30 +11,46 @@ export const dynamic = 'force-dynamic'
1011

1112
const logger = createLogger('OAuthDisconnectAPI')
1213

14+
const disconnectSchema = z.object({
15+
provider: z.string({ required_error: 'Provider is required' }).min(1, 'Provider is required'),
16+
providerId: z.string().optional(),
17+
})
18+
1319
/**
1420
* Disconnect an OAuth provider for the current user
1521
*/
1622
export async function POST(request: NextRequest) {
1723
const requestId = generateRequestId()
1824

1925
try {
20-
// Get the session
2126
const session = await getSession()
2227

23-
// Check if the user is authenticated
2428
if (!session?.user?.id) {
2529
logger.warn(`[${requestId}] Unauthenticated disconnect request rejected`)
2630
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
2731
}
2832

29-
// Get the provider and providerId from the request body
30-
const { provider, providerId } = await request.json()
33+
const rawBody = await request.json()
34+
const parseResult = disconnectSchema.safeParse(rawBody)
35+
36+
if (!parseResult.success) {
37+
const firstError = parseResult.error.errors[0]
38+
const errorMessage = firstError?.message || 'Validation failed'
3139

32-
if (!provider) {
33-
logger.warn(`[${requestId}] Missing provider in disconnect request`)
34-
return NextResponse.json({ error: 'Provider is required' }, { status: 400 })
40+
logger.warn(`[${requestId}] Invalid disconnect request`, {
41+
errors: parseResult.error.errors,
42+
})
43+
44+
return NextResponse.json(
45+
{
46+
error: errorMessage,
47+
},
48+
{ status: 400 }
49+
)
3550
}
3651

52+
const { provider, providerId } = parseResult.data
53+
3754
logger.info(`[${requestId}] Processing OAuth disconnect request`, {
3855
provider,
3956
hasProviderId: !!providerId,

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
23
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
34
import { checkHybridAuth } from '@/lib/auth/hybrid'
45
import { createLogger } from '@/lib/logs/console/logger'
@@ -9,6 +10,22 @@ export const dynamic = 'force-dynamic'
910

1011
const logger = createLogger('OAuthTokenAPI')
1112

13+
const tokenRequestSchema = z.object({
14+
credentialId: z
15+
.string({ required_error: 'Credential ID is required' })
16+
.min(1, 'Credential ID is required'),
17+
workflowId: z.string().min(1, 'Workflow ID is required').nullish(),
18+
})
19+
20+
const tokenQuerySchema = z.object({
21+
credentialId: z
22+
.string({
23+
required_error: 'Credential ID is required',
24+
invalid_type_error: 'Credential ID is required',
25+
})
26+
.min(1, 'Credential ID is required'),
27+
})
28+
1229
/**
1330
* Get an access token for a specific credential
1431
* Supports both session-based authentication (for client-side requests)
@@ -20,19 +37,31 @@ export async function POST(request: NextRequest) {
2037
logger.info(`[${requestId}] OAuth token API POST request received`)
2138

2239
try {
23-
// Parse request body
24-
const body = await request.json()
25-
const { credentialId, workflowId } = body
26-
27-
if (!credentialId) {
28-
logger.warn(`[${requestId}] Credential ID is required`)
29-
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
40+
const rawBody = await request.json()
41+
const parseResult = tokenRequestSchema.safeParse(rawBody)
42+
43+
if (!parseResult.success) {
44+
const firstError = parseResult.error.errors[0]
45+
const errorMessage = firstError?.message || 'Validation failed'
46+
47+
logger.warn(`[${requestId}] Invalid token request`, {
48+
errors: parseResult.error.errors,
49+
})
50+
51+
return NextResponse.json(
52+
{
53+
error: errorMessage,
54+
},
55+
{ status: 400 }
56+
)
3057
}
3158

59+
const { credentialId, workflowId } = parseResult.data
60+
3261
// We already have workflowId from the parsed body; avoid forcing hybrid auth to re-read it
3362
const authz = await authorizeCredentialUse(request, {
3463
credentialId,
35-
workflowId,
64+
workflowId: workflowId ?? undefined,
3665
requireWorkflowIdForInternal: false,
3766
})
3867
if (!authz.ok || !authz.credentialOwnerUserId) {
@@ -63,15 +92,31 @@ export async function GET(request: NextRequest) {
6392
const requestId = generateRequestId()
6493

6594
try {
66-
// Get the credential ID from the query params
6795
const { searchParams } = new URL(request.url)
68-
const credentialId = searchParams.get('credentialId')
96+
const rawQuery = {
97+
credentialId: searchParams.get('credentialId'),
98+
}
6999

70-
if (!credentialId) {
71-
logger.warn(`[${requestId}] Missing credential ID`)
72-
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
100+
const parseResult = tokenQuerySchema.safeParse(rawQuery)
101+
102+
if (!parseResult.success) {
103+
const firstError = parseResult.error.errors[0]
104+
const errorMessage = firstError?.message || 'Validation failed'
105+
106+
logger.warn(`[${requestId}] Invalid query parameters`, {
107+
errors: parseResult.error.errors,
108+
})
109+
110+
return NextResponse.json(
111+
{
112+
error: errorMessage,
113+
},
114+
{ status: 400 }
115+
)
73116
}
74117

118+
const { credentialId } = parseResult.data
119+
75120
// For GET requests, we only support session-based authentication
76121
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
77122
if (!auth.success || auth.authType !== 'session' || !auth.userId) {

0 commit comments

Comments
 (0)