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
10 changes: 5 additions & 5 deletions src/app/api/auth/debug-sms/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function POST(request, { params } = {}) {
if (!isDevelopment()) {
return NextResponse.json({ error: 'Debug endpoints are disabled in production' }, { status: 403 });
}
if (!(await verifyAuth(event))) {
if (!(await verifyAuth(request))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

Expand All @@ -48,7 +48,7 @@ export async function POST(request, { params } = {}) {
);
}

const diagnostics = new SMSAuthDiagnostics(event);
const diagnostics = new SMSAuthDiagnostics(request);

switch (action) {
case 'diagnose':
Expand Down Expand Up @@ -109,12 +109,12 @@ export async function GET(request, { params } = {}) {
if (!isDevelopment()) {
return NextResponse.json({ error: 'Debug endpoints are disabled in production' }, { status: 403 });
}
if (!(await verifyAuth(event))) {
if (!(await verifyAuth(request))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

try {
const diagnostics = new SMSAuthDiagnostics(event);
const diagnostics = new SMSAuthDiagnostics(request);

// Run basic system checks without phone number
const result = await diagnostics.runDiagnostics('+1234567890'); // Dummy number for format check
Expand Down Expand Up @@ -143,4 +143,4 @@ export async function GET(request, { params } = {}) {
{ status: 500 }
);
}
}
}
74 changes: 74 additions & 0 deletions src/app/api/auth/debug-sms/route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
getUser: vi.fn(),
runDiagnostics: vi.fn(),
SMSAuthDiagnostics: vi.fn(function SMSAuthDiagnostics(request) {
this.request = request;
this.logger = { getLogs: () => [] };
this.runDiagnostics = mocks.runDiagnostics;
})
}));

vi.mock('@/lib/supabase.js', () => ({
createSupabaseServerClient: vi.fn(() => ({
auth: {
getUser: mocks.getUser
}
}))
}));

vi.mock('@/lib/utils/sms-debug.js', () => ({
SMSAuthDiagnostics: mocks.SMSAuthDiagnostics
}));

function debugRequest(method, body) {
return new Request('https://example.com/api/auth/debug-sms', {
method,
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined
});
}

describe('SMS debug endpoint', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
vi.stubEnv('NODE_ENV', 'development');
mocks.getUser.mockResolvedValue({
data: { user: { id: 'user-1' } },
error: null
});
mocks.runDiagnostics.mockResolvedValue({
success: true,
issues: [],
logs: []
});
});

it('uses the incoming request for authenticated POST diagnostics', async () => {
const { POST } = await import('./route.js');
const request = debugRequest('POST', { phoneNumber: '+1234567890' });

const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(200);
expect(body.success).toBe(true);
expect(mocks.SMSAuthDiagnostics).toHaveBeenCalledWith(request);
expect(mocks.runDiagnostics).toHaveBeenCalledWith('+1234567890');
});

it('uses the incoming request for authenticated GET diagnostics', async () => {
const { GET } = await import('./route.js');
const request = debugRequest('GET');

const response = await GET(request);
const body = await response.json();

expect(response.status).toBe(200);
expect(body.systemStatus).toBe('healthy');
expect(mocks.SMSAuthDiagnostics).toHaveBeenCalledWith(request);
expect(mocks.runDiagnostics).toHaveBeenCalledWith('+1234567890');
});
});
15 changes: 13 additions & 2 deletions src/app/api/conversations/create/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@ import { MESSAGE_TYPES } from '@/lib/api/protocol.js';

export const POST = withAuth(async ({ request, locals }) => {
try {
const { participantIds, name, isGroup } = await request.json();
let body;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 });
}

const { participantIds, name, isGroup } = body;

if (!participantIds || !Array.isArray(participantIds)) {
return NextResponse.json({ error: 'Missing or invalid participantIds' }, { status: 400 });
Expand Down Expand Up @@ -86,4 +97,4 @@ export const POST = withAuth(async ({ request, locals }) => {
console.error('Create conversation error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
});
});
64 changes: 64 additions & 0 deletions src/app/api/conversations/create/route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
mockSupabase: {
from: vi.fn()
},
mockJoinRoom: vi.fn(),
mockSendToUser: vi.fn()
}));

vi.mock('@/lib/api/middleware/auth.js', () => ({
withAuth: (handler) => (request, context) =>
handler({
request,
locals: {
supabase: mocks.mockSupabase,
user: { id: 'auth-user-id' }
},
context
})
}));

vi.mock('@/lib/api/sse-manager.js', () => ({
sseManager: {
joinRoom: mocks.mockJoinRoom,
sendToUser: mocks.mockSendToUser
}
}));

vi.mock('@/lib/api/protocol.js', () => ({
MESSAGE_TYPES: {
CONVERSATION_CREATED: 'conversation.created'
}
}));

describe('POST /api/conversations/create validation', () => {
it('returns 400 for malformed JSON instead of a generic 500', async () => {
const { POST } = await import('./route.js');
const request = {
json: vi.fn().mockRejectedValue(new SyntaxError('Unexpected token'))
};

const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(400);
expect(body.error).toBe('Invalid JSON body');
expect(mocks.mockSupabase.from).not.toHaveBeenCalled();
});

it('rejects non-object JSON before database work', async () => {
const { POST } = await import('./route.js');
const request = {
json: vi.fn().mockResolvedValue(['user-1'])
};

const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(400);
expect(body.error).toBe('Request body must be a JSON object');
expect(mocks.mockSupabase.from).not.toHaveBeenCalled();
});
});
25 changes: 22 additions & 3 deletions src/app/api/files/upload-complete/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { createSupabaseServerClient } from '@/lib/supabase.js';
import { sseManager } from '@/lib/api/sse-manager.js';
import { MESSAGE_TYPES } from '@/lib/api/protocol.js';

async function parseJsonObject(request) {
let body;
try {
body = await request.json();
} catch {
return { error: NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) };
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return { error: NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 }) };
}

return { body };
}

export async function POST(request, { params } = {}) {
try {
// Create Supabase server client
Expand All @@ -17,12 +32,16 @@ export async function POST(request, { params } = {}) {

console.log(`📁 [UPLOAD-COMPLETE] Upload completion from auth user: ${user.id}`);

// Parse the JSON request body
const parsedBody = await parseJsonObject(request);
if (parsedBody.error) {
return parsedBody.error;
}

const {
storagePath,
fileId,
metadata
} = await request.json();
} = parsedBody.body;

// Validate inputs
if (!storagePath || !fileId || !metadata) {
Expand Down Expand Up @@ -169,4 +188,4 @@ function formatFileSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
69 changes: 69 additions & 0 deletions src/app/api/files/upload-complete/route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
getUser: vi.fn(),
from: vi.fn(),
broadcastToRoom: vi.fn()
}));

vi.mock('@/lib/supabase.js', () => ({
createSupabaseServerClient: vi.fn(() => ({
auth: {
getUser: mocks.getUser
},
from: mocks.from
}))
}));

vi.mock('@/lib/api/sse-manager.js', () => ({
sseManager: {
broadcastToRoom: mocks.broadcastToRoom
}
}));

vi.mock('@/lib/api/protocol.js', () => ({
MESSAGE_TYPES: {
NEW_MESSAGE: 'message.new'
}
}));

describe('POST /api/files/upload-complete validation', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mocks.getUser.mockResolvedValue({
data: { user: { id: 'auth-user-id' } },
error: null
});
});

it('returns 400 for malformed JSON before database work', async () => {
const { POST } = await import('./route.js');
const request = {
json: vi.fn().mockRejectedValue(new SyntaxError('Unexpected token'))
};

const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(400);
expect(body.error).toBe('Invalid JSON body');
expect(mocks.from).not.toHaveBeenCalled();
expect(mocks.broadcastToRoom).not.toHaveBeenCalled();
});

it('rejects non-object JSON before database work', async () => {
const { POST } = await import('./route.js');
const request = {
json: vi.fn().mockResolvedValue('storage-path')
};

const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(400);
expect(body.error).toBe('Request body must be a JSON object');
expect(mocks.from).not.toHaveBeenCalled();
expect(mocks.broadcastToRoom).not.toHaveBeenCalled();
});
});
35 changes: 27 additions & 8 deletions src/app/api/files/upload-url/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
import { NextResponse } from 'next/server';
import { createSupabaseServerClient } from '@/lib/supabase.js';

async function parseJsonObject(request) {
let body;
try {
body = await request.json();
} catch {
return { error: NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) };
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return { error: NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 }) };
}

return { body };
}

export async function POST(request, { params } = {}) {
try {
// Create Supabase server client
Expand All @@ -15,6 +30,17 @@ export async function POST(request, { params } = {}) {

console.log(`📁 [UPLOAD-URL] Signed URL request from auth user: ${user.id}`);

const parsedBody = await parseJsonObject(request);
if (parsedBody.error) {
return parsedBody.error;
}

const {
conversationId,
messageId,
encryptedMetadata
} = parsedBody.body;

// Get the internal user ID from the users table using auth_user_id
const { data: internalUser, error: userError } = await supabase
.from('users')
Expand All @@ -30,13 +56,6 @@ export async function POST(request, { params } = {}) {
const userId = internalUser.id;
console.log(`📁 [UPLOAD-URL] Using internal user ID: ${userId}`);

// Parse the JSON request body (should be small - just encrypted metadata)
const {
conversationId,
messageId,
encryptedMetadata
} = await request.json();

// Validate inputs
if (!conversationId || !messageId || !encryptedMetadata) {
console.error( '📁 [UPLOAD-URL] Missing required fields');
Expand Down Expand Up @@ -116,4 +135,4 @@ export async function POST(request, { params } = {}) {
console.error( '📁 [UPLOAD-URL] ❌ Unexpected error:', err);
return NextResponse.json({ error: 'Internal server error during signed URL generation' }, { status: 500 });
}
}
}
Loading
Loading