Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions app/api/credentials/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { authenticateRequest } from '@/app/lib/auth/authorize';
import { createErrorResponse, createSuccessResponse } from '@/app/lib/api-response';
import { getCredentialStore } from '@/app/lib/database/credential-store';

const UpdateCredentialSchema = z.object({
name: z.string().min(1).optional(),
host: z.string().min(1).optional(),
port: z.number().int().positive().optional(),
database: z.string().min(1).optional(),
username: z.string().min(1).optional(),
password: z.string().min(1).optional(),
ssl: z.boolean().optional(),
schema: z.string().optional(),
warehouse: z.string().optional(),
role: z.string().optional(),
account: z.string().optional(),
});

/**
* GET /api/credentials/[id] - Get a specific credential
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

const { id } = await params;
const store = await getCredentialStore();
const credential = await store.getCredentialById(id, authResult.user.id);

if (!credential) {
return NextResponse.json(
createErrorResponse('Credential not found', 'CREDENTIAL_NOT_FOUND'),
{ status: 404 }
);
}

// Return public credential (without encrypted password)
const { encryptedPassword, ...publicCredential } = credential;
void encryptedPassword; // Suppress unused variable warning

return NextResponse.json(
createSuccessResponse({ credential: publicCredential })
);
} catch (error) {
return NextResponse.json(
createErrorResponse(
error instanceof Error ? error.message : 'Failed to fetch credential',
'FETCH_CREDENTIAL_ERROR'
),
{ status: 500 }
);
}
}

/**
* PUT /api/credentials/[id] - Update a credential
*/
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

const { id } = await params;
const body = await request.json();
const validatedData = UpdateCredentialSchema.parse(body);

const store = await getCredentialStore();
const credential = await store.updateCredential(id, validatedData, authResult.user.id);

return NextResponse.json(
createSuccessResponse({ credential })
);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
createErrorResponse(
'Invalid request data',
'VALIDATION_ERROR',
error.flatten()
),
{ status: 400 }
);
}

if (error instanceof Error && error.message === 'Credential not found') {
return NextResponse.json(
createErrorResponse('Credential not found', 'CREDENTIAL_NOT_FOUND'),
{ status: 404 }
);
}

return NextResponse.json(
createErrorResponse(
error instanceof Error ? error.message : 'Failed to update credential',
'UPDATE_CREDENTIAL_ERROR'
),
{ status: 500 }
);
}
}

/**
* DELETE /api/credentials/[id] - Delete a credential
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

const { id } = await params;
const store = await getCredentialStore();
const deleted = await store.deleteCredential(id, authResult.user.id);

if (!deleted) {
return NextResponse.json(
createErrorResponse('Credential not found', 'CREDENTIAL_NOT_FOUND'),
{ status: 404 }
);
}

return NextResponse.json(
createSuccessResponse({ message: 'Credential deleted successfully' })
);
} catch (error) {
return NextResponse.json(
createErrorResponse(
error instanceof Error ? error.message : 'Failed to delete credential',
'DELETE_CREDENTIAL_ERROR'
),
{ status: 500 }
);
}
}
58 changes: 58 additions & 0 deletions app/api/credentials/[id]/test/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server';
import { authenticateRequest } from '@/app/lib/auth/authorize';
import { createErrorResponse, createSuccessResponse } from '@/app/lib/api-response';
import { getCredentialStore } from '@/app/lib/database/credential-store';
import { testDatabaseConnection } from '@/app/lib/database/connection-tester';

/**
* POST /api/credentials/[id]/test - Test database connection
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

const { id } = await params;
const store = await getCredentialStore();

// Get the credential
const credential = await store.getCredentialById(id, authResult.user.id);
if (!credential) {
return NextResponse.json(
createErrorResponse('Credential not found', 'CREDENTIAL_NOT_FOUND'),
{ status: 404 }
);
}

// Test the connection
const result = await testDatabaseConnection(credential);

return NextResponse.json(
createSuccessResponse({
testResult: result,
credential: {
id: credential.id,
name: credential.name,
type: credential.type,
host: credential.host,
port: credential.port,
database: credential.database,
username: credential.username,
}
})
);
} catch (error) {
return NextResponse.json(
createErrorResponse(
error instanceof Error ? error.message : 'Failed to test connection',
'TEST_CONNECTION_ERROR'
),
{ status: 500 }
);
}
}
92 changes: 92 additions & 0 deletions app/api/credentials/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { authenticateRequest } from '@/app/lib/auth/authorize';
import { createErrorResponse, createSuccessResponse } from '@/app/lib/api-response';
import { getCredentialStore } from '@/app/lib/database/credential-store';
import { DATABASE_TYPES } from '@/app/lib/database/credentials';

// Validation schemas
const CreateCredentialSchema = z.object({
name: z.string().min(1, 'Name is required'),
type: z.enum(DATABASE_TYPES),
host: z.string().min(1, 'Host is required'),
port: z.number().int().positive('Port must be a positive integer'),
database: z.string().min(1, 'Database name is required'),
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
ssl: z.boolean().optional(),
schema: z.string().optional(),
warehouse: z.string().optional(),
role: z.string().optional(),
account: z.string().optional(),
});


/**
* GET /api/credentials - List user's database credentials
*/
export async function GET() {
try {
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

const store = await getCredentialStore();
const credentials = await store.getCredentialsByUserId(authResult.user.id);

return NextResponse.json(
createSuccessResponse({ credentials })
);
} catch (error) {
return NextResponse.json(
createErrorResponse(
error instanceof Error ? error.message : 'Failed to fetch credentials',
'FETCH_CREDENTIALS_ERROR'
),
{ status: 500 }
);
}
}

/**
* POST /api/credentials - Create a new database credential
*/
export async function POST(request: NextRequest) {
try {
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

const body = await request.json();
const validatedData = CreateCredentialSchema.parse(body);

const store = await getCredentialStore();
const credential = await store.createCredential(validatedData, authResult.user.id);

return NextResponse.json(
createSuccessResponse({ credential }),
{ status: 201 }
);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
createErrorResponse(
'Invalid request data',
'VALIDATION_ERROR',
error.flatten()
),
{ status: 400 }
);
}

return NextResponse.json(
createErrorResponse(
error instanceof Error ? error.message : 'Failed to create credential',
'CREATE_CREDENTIAL_ERROR'
),
{ status: 500 }
);
}
}
54 changes: 50 additions & 4 deletions app/api/db/[query]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
} from '@/app/lib/logger';
import { isWriteQuery } from '@/app/lib/sql/operation';
import { authorize } from '@/app/lib/auth/authorize';
import { authenticateRequest } from '@/app/lib/auth/authorize';
import { getCredentialStore } from '@/app/lib/database/credential-store';
import { generateConnectionString } from '@/app/lib/database/credentials';
import { decryptPassword } from '@/app/lib/database/encryption';

/**
* Suggestion helper: generate user-friendly tips based on error details
Expand Down Expand Up @@ -72,6 +76,12 @@ export async function POST(
);
}

// Authenticate user
const authResult = await authenticateRequest();
if (!authResult.ok) {
return authResult.response;
}

// Validate required fields
if (!body.prompt || !body.target) {
log.warn('query.validation_failed', {
Expand Down Expand Up @@ -103,6 +113,34 @@ export async function POST(
);
}

// Handle credential-based queries
let credentialId: string | undefined;
let connectionString: string | undefined;

if (body.credentialId) {
credentialId = body.credentialId;
const store = await getCredentialStore();
const credential = await store.getCredentialById(credentialId, authResult.user.id);

if (!credential) {
return NextResponse.json(
createErrorResponse('Credential not found', 'CREDENTIAL_NOT_FOUND'),
{ status: 404 }
);
}

// Decrypt password and generate connection string
const decryptedPassword = decryptPassword(credential.encryptedPassword);
const connInfo = generateConnectionString(credential, decryptedPassword);
connectionString = connInfo.connectionString;

log.info('query.using_credential', {
credentialId,
credentialName: credential.name,
credentialType: credential.type
});
}

// Check if MCP server is available, otherwise use mock data for development
let mcpData;
let usedMock = false;
Expand All @@ -113,15 +151,23 @@ export async function POST(
prompt: safeTruncate(body.prompt, 1000)
});
try {
const mcpRequestBody: Record<string, unknown> = {
prompt: body.prompt,
target: body.target
};

// Include connection string if using credentials
if (connectionString) {
mcpRequestBody.connectionString = connectionString;
mcpRequestBody.credentialId = credentialId;
}

const mcpResponse = await fetch(MCP_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: body.prompt,
target: body.target
})
body: JSON.stringify(mcpRequestBody)
});

// Check if MCP server responded successfully
Expand Down
Loading