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
56 changes: 28 additions & 28 deletions src/app/api/user/nuclear-delete/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function getBearerToken(authHeader) {
async function authenticateUser(request) {
try {
let accessToken = null;

// First, check for Authorization header (priority)
const authHeader = request.headers.get('authorization');
const bearerToken = getBearerToken(authHeader);
Expand All @@ -47,7 +47,7 @@ async function authenticateUser(request) {

// Try standard cookie names first
accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];

// If not found, look for the base64 encoded auth token format
if (!accessToken) {
for (const [cookieName, cookieValue] of Object.entries(cookies)) {
Expand All @@ -57,7 +57,7 @@ async function authenticateUser(request) {
const base64Data = cookieValue.replace('base64-', '');
const decodedData = Buffer.from(base64Data, 'base64').toString('utf8');
const authData = JSON.parse(decodedData);

// Extract the access token from the decoded data
if (authData.access_token) {
accessToken = authData.access_token;
Expand All @@ -71,7 +71,7 @@ async function authenticateUser(request) {
}
}
}

if (!accessToken) {
return { user: null, error: 'No authentication tokens found' };
}
Expand Down Expand Up @@ -110,46 +110,46 @@ export async function DELETE(request) {
}

console.log('🔐 [API] ✅ User authenticated:', user.id);


// Additional confirmation check - require confirmation in request body
const body = await request.json().catch(() => ({}));
const { confirmation } = body;

if (confirmation !== 'DELETE_ALL_MY_DATA') {
return NextResponse.json({
error: 'Nuclear delete requires explicit confirmation',
required_confirmation: 'DELETE_ALL_MY_DATA'
}, { status: 400 });
}

// Get internal user ID from auth_user_id
const { data: userData, error: userError } = await createServiceRoleClient()
.from('users')
.select('id, phone_number, username')
.eq('auth_user_id', user.id)
.single();

if (userError || !userData) {
console.log('🔐 [API] ❌ Failed to get internal user ID:', userError?.message);
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

const userId = userData.id;
console.log('🔐 [API] ✅ Internal user ID:', userId);

// Additional confirmation check - require confirmation in request body
const body = await request.json().catch(() => ({}));
const { confirmation } = body;

if (confirmation !== 'DELETE_ALL_MY_DATA') {
return NextResponse.json({
error: 'Nuclear delete requires explicit confirmation',
required_confirmation: 'DELETE_ALL_MY_DATA'
}, { status: 400 });
}


console.log(`🔐 [API] Encrypted data delete initiated for user ${userId} (${userData.phone_number})`);

// Call the encrypted data delete function (preserves account)
// Pass both authenticated and target user IDs for security validation
const { data: result, error: deleteError } = await createServiceRoleClient()
.rpc('delete_encrypted_data_only', {
authenticated_user_id: userId, // Who is requesting the deletion
target_user_id: userId // Whose data to delete (must match for security)
});

if (deleteError) {
console.error('Encrypted data delete error:', deleteError);

// Handle specific error cases
if (deleteError.message?.includes('Users can only delete their own data')) {
return NextResponse.json({ error: 'Unauthorized: Can only delete own data' }, { status: 403 });
Expand All @@ -160,19 +160,19 @@ export async function DELETE(request) {
if (deleteError.message?.includes('User not found')) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

return NextResponse.json({
error: 'Encrypted data delete failed',
details: deleteError.message
}, { status: 500 });
}

if (!result) {
return NextResponse.json({ error: 'Encrypted data delete returned no result' }, { status: 500 });
}

console.log(`Encrypted data delete completed for user ${userId}:`, result);

// Return success with deletion summary
return NextResponse.json({
success: true,
Expand All @@ -183,10 +183,10 @@ export async function DELETE(request) {
},
deletion_summary: result
}, { status: 200 });

} catch (error) {
console.error('Nuclear delete API error:', error);
return NextResponse.json({
return NextResponse.json({
error: 'Internal server error',
message: 'An unexpected error occurred during nuclear delete'
}, { status: 500 });
Expand Down
25 changes: 25 additions & 0 deletions src/app/api/user/nuclear-delete/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ function deleteRequest(headers) {
});
}

function deleteRequestWithBody(headers, body) {
return new Request('https://qrypt.chat/api/user/nuclear-delete', {
method: 'DELETE',
headers,
body: JSON.stringify(body)
});
}

function createUserQuery() {
const query = {
select: vi.fn(() => query),
Expand Down Expand Up @@ -102,4 +110,21 @@ describe('DELETE /api/user/nuclear-delete bearer authentication', () => {
expect(response.status).toBe(200);
expect(mocks.authGetUser).toHaveBeenCalledWith('cookie-token');
});

it('requires explicit confirmation before service-role user lookup', async () => {
const { DELETE } = await import('./route.js');
const response = await DELETE(
deleteRequestWithBody({
authorization: 'Bearer access-token',
'content-type': 'application/json'
}, {})
);
const body = await response.json();

expect(response.status).toBe(400);
expect(body.required_confirmation).toBe('DELETE_ALL_MY_DATA');
expect(mocks.authGetUser).toHaveBeenCalledWith('access-token');
expect(mocks.serviceFrom).not.toHaveBeenCalled();
expect(mocks.serviceRpc).not.toHaveBeenCalled();
});
});
Loading