mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-22 21:38:05 -05:00
simplicifcaiton
This commit is contained in:
@@ -3,13 +3,8 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { deleteTable, getTableById, type TableSchema } from '@/lib/table'
|
||||
import {
|
||||
checkTableAccess,
|
||||
checkTableWriteAccess,
|
||||
normalizeColumn,
|
||||
verifyTableWorkspace,
|
||||
} from '../utils'
|
||||
import { deleteTable, type TableSchema } from '@/lib/table'
|
||||
import { accessError, checkAccess, normalizeColumn, verifyTableWorkspace } from '../utils'
|
||||
|
||||
const logger = createLogger('TableDetailAPI')
|
||||
|
||||
@@ -38,33 +33,19 @@ export async function GET(request: NextRequest, { params }: TableRouteParams) {
|
||||
workspaceId: searchParams.get('workspaceId'),
|
||||
})
|
||||
|
||||
const accessCheck = await checkTableAccess(tableId, authResult.userId)
|
||||
const result = await checkAccess(tableId, authResult.userId, 'read')
|
||||
if (!result.ok) return accessError(result, requestId, tableId)
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
if ('notFound' in accessCheck && accessCheck.notFound) {
|
||||
logger.warn(`[${requestId}] Table not found: ${tableId}`)
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} attempted to access unauthorized table ${tableId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
const { table } = result
|
||||
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessCheck.table.workspaceId}`
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
const table = await getTableById(tableId)
|
||||
|
||||
if (!table) {
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Retrieved table ${tableId} for user ${authResult.userId}`)
|
||||
|
||||
const schemaData = table.schema as TableSchema
|
||||
@@ -122,23 +103,15 @@ export async function DELETE(request: NextRequest, { params }: TableRouteParams)
|
||||
workspaceId: searchParams.get('workspaceId'),
|
||||
})
|
||||
|
||||
const accessCheck = await checkTableWriteAccess(tableId, authResult.userId)
|
||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!result.ok) return accessError(result, requestId, tableId)
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
if ('notFound' in accessCheck && accessCheck.notFound) {
|
||||
logger.warn(`[${requestId}] Table not found: ${tableId}`)
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} attempted to delete unauthorized table ${tableId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
const { table } = result
|
||||
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessCheck.table.workspaceId}`
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
@@ -8,12 +8,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import type { RowData, TableSchema } from '@/lib/table'
|
||||
import { validateRowData } from '@/lib/table'
|
||||
import {
|
||||
checkAccessOrRespond,
|
||||
checkTableAccess,
|
||||
getTableById,
|
||||
verifyTableWorkspace,
|
||||
} from '../../../utils'
|
||||
import { accessError, checkAccess, verifyTableWorkspace } from '../../../utils'
|
||||
|
||||
const logger = createLogger('TableRowAPI')
|
||||
|
||||
@@ -50,24 +45,15 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
|
||||
workspaceId: searchParams.get('workspaceId'),
|
||||
})
|
||||
|
||||
const accessCheck = await checkTableAccess(tableId, authResult.userId)
|
||||
const result = await checkAccess(tableId, authResult.userId, 'read')
|
||||
if (!result.ok) return accessError(result, requestId, tableId)
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
if ('notFound' in accessCheck && accessCheck.notFound) {
|
||||
logger.warn(`[${requestId}] Table not found: ${tableId}`)
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} attempted to access row from unauthorized table ${tableId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
const { table } = result
|
||||
|
||||
const actualWorkspaceId = validated.workspaceId
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessCheck.table.workspaceId}`
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
@@ -84,7 +70,7 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
|
||||
and(
|
||||
eq(userTableRows.id, rowId),
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId)
|
||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
@@ -133,23 +119,19 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
|
||||
const body: unknown = await request.json()
|
||||
const validated = UpdateRowSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!result.ok) return accessError(result, requestId, tableId)
|
||||
|
||||
const { table } = result
|
||||
|
||||
const actualWorkspaceId = validated.workspaceId
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessResult.table.workspaceId}`
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
const table = await getTableById(tableId)
|
||||
if (!table) {
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const rowData = validated.data as RowData
|
||||
|
||||
const validation = await validateRowData({
|
||||
@@ -172,7 +154,7 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
|
||||
and(
|
||||
eq(userTableRows.id, rowId),
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId)
|
||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
@@ -222,14 +204,15 @@ export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
|
||||
const body: unknown = await request.json()
|
||||
const validated = DeleteRowSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!result.ok) return accessError(result, requestId, tableId)
|
||||
|
||||
const { table } = result
|
||||
|
||||
const actualWorkspaceId = validated.workspaceId
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessResult.table.workspaceId}`
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
@@ -240,7 +223,7 @@ export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
|
||||
and(
|
||||
eq(userTableRows.id, rowId),
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId)
|
||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
validateUniqueConstraints,
|
||||
} from '@/lib/table'
|
||||
import { buildFilterClause, buildSortClause } from '@/lib/table/query-builder'
|
||||
import { checkAccessOrRespond, checkAccessWithFullTable, verifyTableWorkspace } from '../../utils'
|
||||
import { accessError, checkAccess, verifyTableWorkspace } from '../../utils'
|
||||
|
||||
const logger = createLogger('TableRowsAPI')
|
||||
|
||||
@@ -88,10 +88,10 @@ async function handleBatchInsert(
|
||||
): Promise<NextResponse> {
|
||||
const validated = BatchInsertRowsSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessWithFullTable(tableId, userId, requestId, 'write')
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const accessResult = await checkAccess(tableId, userId, 'write')
|
||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
||||
|
||||
const table = accessResult.table
|
||||
const { table } = accessResult
|
||||
|
||||
if (validated.workspaceId !== table.workspaceId) {
|
||||
logger.warn(
|
||||
@@ -178,15 +178,10 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
|
||||
|
||||
const validated = InsertRowSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessWithFullTable(
|
||||
tableId,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
'write'
|
||||
)
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
||||
|
||||
const table = accessResult.table
|
||||
const { table } = accessResult
|
||||
|
||||
if (validated.workspaceId !== table.workspaceId) {
|
||||
logger.warn(
|
||||
@@ -295,13 +290,8 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
|
||||
offset,
|
||||
})
|
||||
|
||||
const accessResult = await checkAccessWithFullTable(
|
||||
tableId,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
'read'
|
||||
)
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const accessResult = await checkAccess(tableId, authResult.userId, 'read')
|
||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
||||
|
||||
const { table } = accessResult
|
||||
|
||||
@@ -400,15 +390,10 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
|
||||
const body: unknown = await request.json()
|
||||
const validated = UpdateRowsByFilterSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessWithFullTable(
|
||||
tableId,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
'write'
|
||||
)
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
||||
|
||||
const table = accessResult.table
|
||||
const { table } = accessResult
|
||||
|
||||
if (validated.workspaceId !== table.workspaceId) {
|
||||
logger.warn(
|
||||
@@ -583,13 +568,15 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
|
||||
const body: unknown = await request.json()
|
||||
const validated = DeleteRowsByFilterSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
||||
|
||||
const { table } = accessResult
|
||||
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessResult.table.workspaceId}`
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import type { RowData, TableSchema } from '@/lib/table'
|
||||
import { getUniqueColumns, validateRowData } from '@/lib/table'
|
||||
import { checkAccessOrRespond, getTableById, verifyTableWorkspace } from '../../../utils'
|
||||
import { accessError, checkAccess, verifyTableWorkspace } from '../../../utils'
|
||||
|
||||
const logger = createLogger('TableUpsertAPI')
|
||||
|
||||
@@ -35,23 +35,17 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
|
||||
const body: unknown = await request.json()
|
||||
const validated = UpsertRowSchema.parse(body)
|
||||
|
||||
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
|
||||
if (accessResult instanceof NextResponse) return accessResult
|
||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
||||
if (!result.ok) return accessError(result, requestId, tableId)
|
||||
|
||||
const actualWorkspaceId = validated.workspaceId || accessResult.table.workspaceId
|
||||
if (validated.workspaceId) {
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${accessResult.table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
const { table } = result
|
||||
|
||||
const table = await getTableById(tableId)
|
||||
if (!table) {
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
const schema = table.schema as TableSchema
|
||||
@@ -102,7 +96,7 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
|
||||
.where(
|
||||
and(
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId),
|
||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
||||
...validUniqueFilters
|
||||
)
|
||||
)
|
||||
@@ -110,7 +104,7 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const result = await db.transaction(async (trx) => {
|
||||
const upsertResult = await db.transaction(async (trx) => {
|
||||
if (existingRow) {
|
||||
const [updatedRow] = await trx
|
||||
.update(userTableRows)
|
||||
@@ -132,7 +126,7 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
|
||||
.values({
|
||||
id: `row_${crypto.randomUUID().replace(/-/g, '')}`,
|
||||
tableId,
|
||||
workspaceId: actualWorkspaceId,
|
||||
workspaceId: validated.workspaceId,
|
||||
data: validated.data,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -147,20 +141,20 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Upserted (${result.operation}) row ${result.row.id} in table ${tableId}`
|
||||
`[${requestId}] Upserted (${upsertResult.operation}) row ${upsertResult.row.id} in table ${tableId}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
row: {
|
||||
id: result.row.id,
|
||||
data: result.row.data,
|
||||
createdAt: result.row.createdAt.toISOString(),
|
||||
updatedAt: result.row.updatedAt.toISOString(),
|
||||
id: upsertResult.row.id,
|
||||
data: upsertResult.row.data,
|
||||
createdAt: upsertResult.row.createdAt.toISOString(),
|
||||
updatedAt: upsertResult.row.updatedAt.toISOString(),
|
||||
},
|
||||
operation: result.operation,
|
||||
message: `Row ${result.operation === 'update' ? 'updated' : 'inserted'} successfully`,
|
||||
operation: upsertResult.operation,
|
||||
message: `Row ${upsertResult.operation === 'update' ? 'updated' : 'inserted'} successfully`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,196 +1,56 @@
|
||||
import { db } from '@sim/db'
|
||||
import { userTableDefinitions, userTableRows } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { count, eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { ColumnDefinition, TableDefinition } from '@/lib/table'
|
||||
import { getTableById } from '@/lib/table'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('TableUtils')
|
||||
|
||||
type PermissionLevel = 'read' | 'write' | 'admin'
|
||||
|
||||
/** @deprecated Use TableDefinition from '@/lib/table' instead */
|
||||
export type TableData = TableDefinition
|
||||
|
||||
export interface TableAccessResult {
|
||||
hasAccess: true
|
||||
table: Pick<TableDefinition, 'id' | 'workspaceId' | 'createdBy'>
|
||||
}
|
||||
|
||||
export interface TableAccessResultFull {
|
||||
hasAccess: true
|
||||
table: TableDefinition
|
||||
}
|
||||
|
||||
export interface TableAccessDenied {
|
||||
hasAccess: false
|
||||
notFound?: boolean
|
||||
reason?: string
|
||||
}
|
||||
export type AccessResult = { ok: true; table: TableDefinition } | { ok: false; status: 404 | 403 }
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
error: string
|
||||
details?: unknown
|
||||
}
|
||||
|
||||
export async function checkTableAccess(
|
||||
tableId: string,
|
||||
userId: string
|
||||
): Promise<TableAccessResult | TableAccessDenied> {
|
||||
return checkTableAccessInternal(tableId, userId, 'read')
|
||||
}
|
||||
|
||||
export async function checkTableWriteAccess(
|
||||
tableId: string,
|
||||
userId: string
|
||||
): Promise<TableAccessResult | TableAccessDenied> {
|
||||
return checkTableAccessInternal(tableId, userId, 'write')
|
||||
}
|
||||
|
||||
export async function checkAccessOrRespond(
|
||||
export async function checkAccess(
|
||||
tableId: string,
|
||||
userId: string,
|
||||
requestId: string,
|
||||
level: 'read' | 'write' | 'admin' = 'write'
|
||||
): Promise<TableAccessResult | NextResponse> {
|
||||
const checkFn = level === 'read' ? checkTableAccess : checkTableWriteAccess
|
||||
const accessCheck = await checkFn(tableId, userId)
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
if ('notFound' in accessCheck && accessCheck.notFound) {
|
||||
logger.warn(`[${requestId}] Table not found: ${tableId}`)
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
logger.warn(`[${requestId}] User ${userId} denied ${level} access to table ${tableId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
return accessCheck
|
||||
}
|
||||
|
||||
export async function checkAccessWithFullTable(
|
||||
tableId: string,
|
||||
userId: string,
|
||||
requestId: string,
|
||||
level: 'read' | 'write' | 'admin' = 'write'
|
||||
): Promise<TableAccessResultFull | NextResponse> {
|
||||
const [tableData] = await db
|
||||
.select()
|
||||
.from(userTableDefinitions)
|
||||
.where(eq(userTableDefinitions.id, tableId))
|
||||
.limit(1)
|
||||
|
||||
if (!tableData) {
|
||||
logger.warn(`[${requestId}] Table not found: ${tableId}`)
|
||||
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const rowCount = await getTableRowCount(tableId)
|
||||
const table = { ...tableData, rowCount } as unknown as TableDefinition
|
||||
|
||||
if (table.createdBy === userId) {
|
||||
return { hasAccess: true, table }
|
||||
}
|
||||
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
|
||||
|
||||
if (!hasPermissionLevel(userPermission, level)) {
|
||||
logger.warn(`[${requestId}] User ${userId} denied ${level} access to table ${tableId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
return { hasAccess: true, table }
|
||||
}
|
||||
|
||||
export async function getTableById(tableId: string): Promise<TableDefinition | null> {
|
||||
const [table] = await db
|
||||
.select()
|
||||
.from(userTableDefinitions)
|
||||
.where(eq(userTableDefinitions.id, tableId))
|
||||
.limit(1)
|
||||
level: 'read' | 'write' | 'admin' = 'read'
|
||||
): Promise<AccessResult> {
|
||||
const table = await getTableById(tableId)
|
||||
|
||||
if (!table) {
|
||||
return null
|
||||
return { ok: false, status: 404 }
|
||||
}
|
||||
|
||||
const rowCount = await getTableRowCount(tableId)
|
||||
return { ...table, rowCount } as unknown as TableDefinition
|
||||
if (table.createdBy === userId) {
|
||||
return { ok: true, table }
|
||||
}
|
||||
|
||||
const permission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
|
||||
const hasAccess =
|
||||
permission !== null &&
|
||||
(level === 'read' ||
|
||||
(level === 'write' && (permission === 'write' || permission === 'admin')) ||
|
||||
(level === 'admin' && permission === 'admin'))
|
||||
|
||||
return hasAccess ? { ok: true, table } : { ok: false, status: 403 }
|
||||
}
|
||||
|
||||
export function accessError(
|
||||
result: { ok: false; status: 404 | 403 },
|
||||
requestId: string,
|
||||
context?: string
|
||||
): NextResponse {
|
||||
const message = result.status === 404 ? 'Table not found' : 'Access denied'
|
||||
logger.warn(`[${requestId}] ${message}${context ? `: ${context}` : ''}`)
|
||||
return NextResponse.json({ error: message }, { status: result.status })
|
||||
}
|
||||
|
||||
export async function verifyTableWorkspace(tableId: string, workspaceId: string): Promise<boolean> {
|
||||
const table = await db
|
||||
.select({ workspaceId: userTableDefinitions.workspaceId })
|
||||
.from(userTableDefinitions)
|
||||
.where(eq(userTableDefinitions.id, tableId))
|
||||
.limit(1)
|
||||
|
||||
if (table.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return table[0].workspaceId === workspaceId
|
||||
}
|
||||
|
||||
async function checkTableAccessInternal(
|
||||
tableId: string,
|
||||
userId: string,
|
||||
requiredLevel: 'read' | 'write' | 'admin'
|
||||
): Promise<TableAccessResult | TableAccessDenied> {
|
||||
const table = await db
|
||||
.select({
|
||||
id: userTableDefinitions.id,
|
||||
createdBy: userTableDefinitions.createdBy,
|
||||
workspaceId: userTableDefinitions.workspaceId,
|
||||
})
|
||||
.from(userTableDefinitions)
|
||||
.where(eq(userTableDefinitions.id, tableId))
|
||||
.limit(1)
|
||||
|
||||
if (table.length === 0) {
|
||||
return { hasAccess: false, notFound: true }
|
||||
}
|
||||
|
||||
const tableData = table[0]
|
||||
|
||||
if (tableData.createdBy === userId) {
|
||||
return { hasAccess: true, table: tableData }
|
||||
}
|
||||
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', tableData.workspaceId)
|
||||
|
||||
if (hasPermissionLevel(userPermission, requiredLevel)) {
|
||||
return { hasAccess: true, table: tableData }
|
||||
}
|
||||
|
||||
return { hasAccess: false }
|
||||
}
|
||||
|
||||
function hasPermissionLevel(
|
||||
userPermission: 'read' | 'write' | 'admin' | null,
|
||||
requiredLevel: PermissionLevel
|
||||
): boolean {
|
||||
if (userPermission === null) return false
|
||||
|
||||
switch (requiredLevel) {
|
||||
case 'read':
|
||||
return true
|
||||
case 'write':
|
||||
return userPermission === 'write' || userPermission === 'admin'
|
||||
case 'admin':
|
||||
return userPermission === 'admin'
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function getTableRowCount(tableId: string): Promise<number> {
|
||||
const [result] = await db
|
||||
.select({ count: count() })
|
||||
.from(userTableRows)
|
||||
.where(eq(userTableRows.tableId, tableId))
|
||||
|
||||
return Number(result?.count ?? 0)
|
||||
const table = await getTableById(tableId)
|
||||
return table?.workspaceId === workspaceId
|
||||
}
|
||||
|
||||
export function errorResponse(
|
||||
@@ -205,30 +65,23 @@ export function errorResponse(
|
||||
return NextResponse.json(body, { status })
|
||||
}
|
||||
|
||||
export function badRequestResponse(
|
||||
message: string,
|
||||
details?: unknown
|
||||
): NextResponse<ApiErrorResponse> {
|
||||
export function badRequestResponse(message: string, details?: unknown) {
|
||||
return errorResponse(message, 400, details)
|
||||
}
|
||||
|
||||
export function unauthorizedResponse(
|
||||
message = 'Authentication required'
|
||||
): NextResponse<ApiErrorResponse> {
|
||||
export function unauthorizedResponse(message = 'Authentication required') {
|
||||
return errorResponse(message, 401)
|
||||
}
|
||||
|
||||
export function forbiddenResponse(message = 'Access denied'): NextResponse<ApiErrorResponse> {
|
||||
export function forbiddenResponse(message = 'Access denied') {
|
||||
return errorResponse(message, 403)
|
||||
}
|
||||
|
||||
export function notFoundResponse(message = 'Resource not found'): NextResponse<ApiErrorResponse> {
|
||||
export function notFoundResponse(message = 'Resource not found') {
|
||||
return errorResponse(message, 404)
|
||||
}
|
||||
|
||||
export function serverErrorResponse(
|
||||
message = 'Internal server error'
|
||||
): NextResponse<ApiErrorResponse> {
|
||||
export function serverErrorResponse(message = 'Internal server error') {
|
||||
return errorResponse(message, 500)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user