diff --git a/package.json b/package.json index 38ed58e..9de986c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dayjs": "^1.11.13", "downshift": "^9.0.9", "embla-carousel-react": "^8.6.0", + "file-type": "^20.4.1", "jsqr": "^1.4.0", "next": "15.2.4", "next-auth": "5.0.0-beta.25", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d436b7..727a73a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.1.0) + file-type: + specifier: ^20.4.1 + version: 20.4.1 jsqr: specifier: ^1.4.0 version: 1.4.0 @@ -792,6 +795,13 @@ packages: '@tailwindcss/postcss@4.1.3': resolution: {integrity: sha512-6s5nJODm98F++QT49qn8xJKHQRamhYHfMi3X7/ltxiSQ9dyRsaFSfFkfaMsanWzf+TMYQtbk8mt5f6cCVXJwfg==} + '@tokenizer/inflate@0.2.7': + resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@trafficlunar/asmcrypto.js@1.0.2': resolution: {integrity: sha512-Iv0EnYsr8PdZo4iipddzZAV790VbQoDAEr9ZdNvwlrtaAZDoSl5ivweMOBEHV2smMzFqkRlSQIyo+HKUPyFkjQ==} @@ -1382,6 +1392,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1390,6 +1403,10 @@ packages: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} + file-type@20.4.1: + resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} + engines: {node: '>=18'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1492,6 +1509,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1879,6 +1899,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + peek-readable@7.0.0: + resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==} + engines: {node: '>=18'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2132,6 +2156,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@10.2.2: + resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} + engines: {node: '>=18'} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -2176,6 +2204,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + token-types@6.0.0: + resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} + engines: {node: '>=14.16'} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2213,6 +2245,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + uint8array-extras@1.4.0: + resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + engines: {node: '>=18'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -2788,6 +2824,16 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.1.3 + '@tokenizer/inflate@0.2.7': + dependencies: + debug: 4.4.0 + fflate: 0.8.2 + token-types: 6.0.0 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@trafficlunar/asmcrypto.js@1.0.2': {} '@tybys/wasm-util@0.9.0': @@ -3572,6 +3618,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -3580,6 +3628,15 @@ snapshots: dependencies: tslib: 2.8.1 + file-type@20.4.1: + dependencies: + '@tokenizer/inflate': 0.2.7 + strtok3: 10.2.2 + token-types: 6.0.0 + uint8array-extras: 1.4.0 + transitivePeerDependencies: + - supports-color + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3687,6 +3744,8 @@ snapshots: dependencies: function-bind: 1.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.1: @@ -4058,6 +4117,8 @@ snapshots: path-parse@1.0.7: {} + peek-readable@7.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4390,6 +4451,11 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@10.2.2: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 7.0.0 + styled-jsx@5.1.6(react@19.1.0): dependencies: client-only: 0.0.1 @@ -4422,6 +4488,11 @@ snapshots: dependencies: is-number: 7.0.0 + token-types@6.0.0: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -4474,6 +4545,8 @@ snapshots: typescript@5.8.3: {} + uint8array-extras@1.4.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 diff --git a/src/lib/images.ts b/src/lib/images.ts index 3da7a31..fdacfca 100644 --- a/src/lib/images.ts +++ b/src/lib/images.ts @@ -1,6 +1,7 @@ // import * as tf from "@tensorflow/tfjs-node"; // import * as nsfwjs from "nsfwjs"; import sharp from "sharp"; +import { fileTypeFromBuffer } from "file-type"; const MIN_IMAGE_DIMENSIONS = 128; const MAX_IMAGE_DIMENSIONS = 1024; @@ -23,12 +24,16 @@ const MAX_IMAGE_SIZE = 1024 * 1024; // 1 MB export async function validateImage(file: File): Promise<{ valid: boolean; error?: string; status?: number }> { if (!file || file.size == 0) return { valid: false, error: "Empty image file" }; - if (!file.type.startsWith("image/")) return { valid: false, error: "Invalid file type. Only images are allowed" }; if (file.size > MAX_IMAGE_SIZE) return { valid: false, error: `One or more of your images are too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` }; try { const buffer = Buffer.from(await file.arrayBuffer()); + + // Check mime type + const fileType = await fileTypeFromBuffer(buffer); + if (!fileType || !fileType.mime.startsWith("image/")) return { valid: false, error: "Invalid image file type. Only actual images are allowed" }; + const metadata = await sharp(buffer).metadata(); // Check image dimensions