diff --git a/.env b/.env index 1a6b274..bac3bec 100644 --- a/.env +++ b/.env @@ -17,6 +17,10 @@ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com # `DATABASE_URL` is a placeholder, you can find your connection string in `.env.local` file. DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres +# Admin Panel JWT Secret (REQUIRED for admin panel authentication) +# Generate a secure secret with: openssl rand -base64 32 +JWT_SECRET=your-secret-key-change-in-production + # Next.js NEXT_TELEMETRY_DISABLED=1 diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx index 887dc3c..ac4721c 100644 --- a/src/app/admin/dashboard/page.tsx +++ b/src/app/admin/dashboard/page.tsx @@ -93,19 +93,18 @@ export default function AdminDashboard() { setQueryResult(null); try { - // Use parameterized query through the query API to prevent SQL injection - const response = await fetch('/api/admin/query', { + // Use dedicated API with table name validation + const response = await fetch('/api/admin/table-data', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - query: `SELECT * FROM "${tableName}" LIMIT 100`, - }), + body: JSON.stringify({ tableName }), }); if (!response.ok) { - throw new Error('Query failed'); + const data = await response.json(); + throw new Error(data.error || 'Query failed'); } const data = await response.json(); diff --git a/src/app/api/admin/table-data/route.ts b/src/app/api/admin/table-data/route.ts new file mode 100644 index 0000000..e14f727 --- /dev/null +++ b/src/app/api/admin/table-data/route.ts @@ -0,0 +1,60 @@ +import { sql } from 'drizzle-orm'; +import { NextResponse } from 'next/server'; +import { db } from '@/utils/db'; +import { getSession } from '@/utils/session'; + +export async function POST(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName } = await request.json(); + + if (!tableName) { + return NextResponse.json( + { error: 'Table name is required' }, + { status: 400 }, + ); + } + + // Validate table name against schema to prevent SQL injection + const tablesResult = await db.execute(sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + `); + + if (tablesResult.rows.length === 0) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + // Table name is validated, sanitize and use + const safeTableName = String(tableName).replace(/\W/g, ''); + const result = await db.execute(sql.raw(`SELECT * FROM "${safeTableName}" LIMIT 100`)); + + return NextResponse.json({ + rows: result.rows, + rowCount: result.rowCount, + fields: result.fields?.map(field => ({ + name: field.name, + dataTypeID: field.dataTypeID, + })) || [], + }); + } catch (error: any) { + console.error('Table query error:', error); + return NextResponse.json( + { error: error.message || 'Query failed' }, + { status: 500 }, + ); + } +} diff --git a/src/utils/session.ts b/src/utils/session.ts index fad613d..56a0baa 100644 --- a/src/utils/session.ts +++ b/src/utils/session.ts @@ -3,16 +3,12 @@ import { cookies } from 'next/headers'; const SESSION_COOKIE_NAME = 'admin-session'; -// Get JWT secret and throw error if not provided in production +// Get JWT secret and throw error if not provided function getJwtSecret(): Uint8Array { const secret = process.env.JWT_SECRET; if (!secret) { - if (process.env.NODE_ENV === 'production') { - throw new Error('JWT_SECRET environment variable is required in production'); - } - console.warn('JWT_SECRET not set, using development default'); - return new TextEncoder().encode('development-secret-change-in-production'); + throw new Error('JWT_SECRET environment variable is required'); } return new TextEncoder().encode(secret);