refactor: Improve type safety in download and file routes by updating type annotations
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import { RowDataPacket } from 'mysql2/promise';
|
import { RowDataPacket } from 'mysql2/promise';
|
||||||
|
|
||||||
@@ -6,15 +6,20 @@ interface FilePart extends RowDataPacket {
|
|||||||
discord_message_id: string | null;
|
discord_message_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RouteContext {
|
||||||
|
params: {
|
||||||
|
file_id: string;
|
||||||
|
part_index: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||||
throw new Error('Discord bot token or channel ID is not configured');
|
throw new Error('Discord bot token or channel ID is not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: Request, { params }: { params: { file_id: string; part_index: string; } }) {
|
export async function GET(request: NextRequest, context: { params: Record<string, string | string[]> }) {
|
||||||
try {
|
try {
|
||||||
const params = await context.params;
|
const { file_id, part_index } = params;
|
||||||
const file_id = params.file_id as string;
|
|
||||||
const part_index = params.part_index as string;
|
|
||||||
|
|
||||||
// NOTE: In a real-world scenario, you MUST validate if the user has permission to download this part.
|
// NOTE: In a real-world scenario, you MUST validate if the user has permission to download this part.
|
||||||
// This would involve checking the download_token against the hash in the `files` table.
|
// This would involve checking the download_token against the hash in the `files` table.
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
import { RowDataPacket } from 'mysql2/promise';
|
||||||
|
|
||||||
export async function GET(request: Request, context: any) {
|
interface FileData extends RowDataPacket {
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
num_parts: number;
|
||||||
|
expires_at: Date | null;
|
||||||
|
token_hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilePartMetadata extends RowDataPacket {
|
||||||
|
part_index: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: { file_id: string; } }) {
|
||||||
try {
|
try {
|
||||||
const params = await context.params;
|
const file_id = params.file_id;
|
||||||
const file_id = params.file_id as string;
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
@@ -16,8 +29,8 @@ export async function GET(request: Request, context: any) {
|
|||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
// 1. Fetch file metadata and token hash
|
// 1. Fetch file metadata and token hash
|
||||||
const [fileRows]: any[] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
const [fileRows]: [RowDataPacket[], unknown] = await connection.query('SELECT * FROM files WHERE id = ? AND deleted = 0', [file_id]);
|
||||||
const file = fileRows[0];
|
const file = fileRows[0] as FileData;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
||||||
@@ -35,7 +48,7 @@ export async function GET(request: Request, context: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Fetch file parts
|
// 4. Fetch file parts
|
||||||
const [partsRows]: any[] = await connection.query(
|
const [partsRows]: [RowDataPacket[], unknown] = await connection.query(
|
||||||
'SELECT part_index, size FROM file_parts WHERE file_id = ? ORDER BY part_index ASC',
|
'SELECT part_index, size FROM file_parts WHERE file_id = ? ORDER BY part_index ASC',
|
||||||
[file_id]
|
[file_id]
|
||||||
);
|
);
|
||||||
@@ -45,7 +58,7 @@ export async function GET(request: Request, context: any) {
|
|||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
num_parts: file.num_parts,
|
num_parts: file.num_parts,
|
||||||
parts: partsRows,
|
parts: partsRows[0] as FilePartMetadata[],
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json(response);
|
return NextResponse.json(response);
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { useParams, useSearchParams } from 'next/navigation';
|
import { useParams, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
interface MetadataPart {
|
||||||
|
part_index: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to convert Base64 string back to ArrayBuffer
|
// Helper to convert Base64 string back to ArrayBuffer
|
||||||
function base64ToBuffer(base64: string) {
|
function base64ToBuffer(base64: string) {
|
||||||
const binary_string = window.atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
|
const binary_string = window.atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||||
@@ -106,7 +111,7 @@ export default function DownloadPage() {
|
|||||||
setDownloadState('downloading');
|
setDownloadState('downloading');
|
||||||
const encryptedParts = new Array(metadata.num_parts);
|
const encryptedParts = new Array(metadata.num_parts);
|
||||||
|
|
||||||
const downloadPromises = metadata.parts.map(async (part: any) => {
|
const downloadPromises = metadata.parts.map(async (part: MetadataPart) => {
|
||||||
const response = await fetchWithRetry(`/api/download-part/${file_id}/${part.part_index}`);
|
const response = await fetchWithRetry(`/api/download-part/${file_id}/${part.part_index}`);
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
encryptedParts[part.part_index] = { index: part.part_index, data: buffer };
|
encryptedParts[part.part_index] = { index: part.part_index, data: buffer };
|
||||||
@@ -127,7 +132,7 @@ export default function DownloadPage() {
|
|||||||
['decrypt']
|
['decrypt']
|
||||||
);
|
);
|
||||||
|
|
||||||
const decryptPromises = encryptedParts.map(async (part) => {
|
const decryptPromises = encryptedParts.map(async (part: { index: number; data: ArrayBuffer }) => {
|
||||||
const iv = part.data.slice(0, 12);
|
const iv = part.data.slice(0, 12);
|
||||||
const data = part.data.slice(12);
|
const data = part.data.slice(12);
|
||||||
const decrypted = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, fileKey, data);
|
const decrypted = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, fileKey, data);
|
||||||
|
|||||||
Reference in New Issue
Block a user