diff --git a/README.md b/README.md index f95096c..0b99d32 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Have a look at the Dockerfile for a full example of how to run this application. - `SECRET_REDIS_PASSWORD` Default: "" - Your redis password - `SECRET_JWT_SECRET` Default: good_luck_have_fun - Override this for the secret signin JWT tokens for log in - `SECRET_FILE_SIZE` Default: 4 - Set the allowed upload file size in mb. +- `SECRET_FILE_LIMIT` Default: 3 - Set the amount of files allowed to be uploaded - `SECRET_ENABLE_FILE_UPLOAD` Default: true - Enable or disable file upload - `SECRET_DO_SPACES_ENDPOINT` Default: "" - The Spaces/s3 endpoint - `SECRET_DO_SPACES_KEY` Default: "" - The Spaces/s3 key diff --git a/config/default.cjs b/config/default.cjs index 92e8813..0557169 100644 --- a/config/default.cjs +++ b/config/default.cjs @@ -11,6 +11,7 @@ const { SECRET_JWT_SECRET = 'good_luck_have_fun', SECRET_ENABLE_FILE_UPLOAD = 'true', SECRET_FILE_SIZE = 4, // 4 mb + SECRET_FILE_LIMIT = 3, // maximum of 3 files allowed to be uploaded SECRET_DO_SPACES_ENDPOINT = 'https://fra1.digitaloceanspaces.com', SECRET_DO_SPACES_KEY = '', SECRET_DO_SPACES_SECRET = '', @@ -30,6 +31,7 @@ const config = { file: { size: SECRET_FILE_SIZE, adapter: !!SECRET_DO_SPACES_SECRET ? 'do' : 'disk', + limit: SECRET_FILE_LIMIT, }, redis: { host: SECRET_REDIS_HOST, diff --git a/server.js b/server.js index fb31633..f9fb7d8 100644 --- a/server.js +++ b/server.js @@ -16,7 +16,7 @@ import keyGeneration from './src/server/decorators/key-generation.js'; import authenticationRoute from './src/server/controllers/authentication.js'; import accountRoute from './src/server/controllers/account.js'; -import uploadRoute from './src/server/controllers/upload.js'; +import downloadROute from './src/server/controllers/download.js'; import secretRoute from './src/server/controllers/secret.js'; import statsRoute from './src/server/controllers/stats.js'; import healthzRoute from './src/server/controllers/healthz.js'; @@ -38,7 +38,7 @@ fastify.register(cors, { origin: config.get('cors') }); fastify.register(multipart, { attachFieldsToBody: true, limits: { - files: 1, + files: config.get('file.limit'), fileSize: MAX_FILE_BYTES, }, }); @@ -59,7 +59,7 @@ fastify.register(authenticationRoute, { fastify.register(accountRoute, { prefix: '/api/account', }); -fastify.register(uploadRoute, { prefix: '/api/upload' }); +fastify.register(downloadROute, { prefix: '/api/download' }); fastify.register(secretRoute, { prefix: '/api/secret' }); fastify.register(statsRoute, { prefix: '/api/stats' }); fastify.register(healthzRoute, { prefix: '/api/healthz' }); diff --git a/src/client/api/secret.js b/src/client/api/secret.js index 672a9ab..18ab4ba 100644 --- a/src/client/api/secret.js +++ b/src/client/api/secret.js @@ -14,11 +14,14 @@ export const createSecret = async (formData = {}, token = '') => { }, }); } + try { + const data = await fetch(`${config.get('api.host')}/secret`, options); + const json = await data.json(); - const data = await fetch(`${config.get('api.host')}/secret`, options); - const json = await data.json(); - - return { ...json, statusCode: data.status }; + return { ...json, statusCode: data.status }; + } catch (e) { + console.error(e); + } }; export const getSecret = async (secretId, encryptionKey, password) => { diff --git a/src/client/api/upload.js b/src/client/api/upload.js index 2503370..1fc949d 100644 --- a/src/client/api/upload.js +++ b/src/client/api/upload.js @@ -1,30 +1,34 @@ import config from '../config'; export const downloadFile = async (fileData, token) => { - const { key, encryptionKey, ext, mime, secretId } = fileData; + const { files, encryptionKey, secretId } = fileData; - const data = await fetch(`${config.get('api.host')}/upload/download`, { - method: 'POST', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ key, encryptionKey, ext, mime, secretId }), - }); + files.forEach(async (file) => { + const { key, ext, mime } = file; + + const data = await fetch(`${config.get('api.host')}/download/`, { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ key, encryptionKey, ext, mime, secretId }), + }); - if (data.status === 401) { - return { - statusCode: 401, - }; - } + if (data.status === 401) { + return { + statusCode: 401, + }; + } - const blob = await data.blob(); - const imageUrl = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = imageUrl; - link.download = `${secretId}.${ext}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const blob = await data.blob(); + const imageUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `${secretId}.${ext}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); }; diff --git a/src/client/routes/home/index.js b/src/client/routes/home/index.js index 7369c65..3486d3d 100644 --- a/src/client/routes/home/index.js +++ b/src/client/routes/home/index.js @@ -18,6 +18,7 @@ import { Divider, FileButton, NumberInput, + Badge, } from '@mantine/core'; import { useMediaQuery } from '@mantine/hooks'; import { @@ -46,7 +47,7 @@ const Home = () => { const [title, setTitle] = useState(''); const [maxViews, setMaxViews] = useState(1); const [enableFileUpload] = useState(config.get('settings.enableFileUpload', false)); - const [file, setFile] = useState(null); + const [files, setFiles] = useState([]); const [ttl, setTTL] = useState(14400); const [password, setPassword] = useState(''); const [enablePassword, setOnEnablePassword] = useState(false); @@ -126,7 +127,7 @@ const Home = () => { setPassword(''); setEncryptionKey(''); setAllowedIp(''); - setFile(''); + setFiles([]); setTitle(''); setPreventBurn(false); setFormData(new FormData()); @@ -147,7 +148,6 @@ const Home = () => { event.preventDefault(); formData.append('text', text); - formData.append('file', file); formData.append('title', title); formData.append('password', password); formData.append('ttl', ttl); @@ -155,12 +155,16 @@ const Home = () => { formData.append('preventBurn', preventBurn); formData.append('maxViews', maxViews); + files.forEach((file) => formData.append('files[]', file)); + const json = await createSecret(formData, getToken()); if (json.statusCode !== 201) { - setError( - json.error === 'Payload Too Large' ? 'The file size is too large' : json.error - ); + if (json.message === 'request file too large, please check multipart config') { + setError('The file size is too large'); + } else { + setError(json.error); + } setCreatingSecret(false); @@ -358,7 +362,12 @@ const Home = () => { {enableFileUpload && ( - + {(props) => ( )} @@ -385,14 +394,18 @@ const Home = () => { Sign in to upload files )} - - {file && ( - - Picked file: {file.name} - - )} + {files.length > 0 && ( + + {files.map((file) => ( + + {file.name} + + ))} + + )} + {secretId && ( { const [isSecretOpen, setIsSecretOpen] = useState(false); const [password, setPassword] = useState(''); const [isPasswordRequired, setIsPasswordRequired] = useState(false); - const [file, setFile] = useState(null); + const [files, setFiles] = useState(null); const [isDownloaded, setIsDownloaded] = useState(false); const [error, setError] = useState(null); const [hasConvertedBase64ToPlain, setHasConvertedBase64ToPlain] = useState(false); @@ -64,8 +64,8 @@ const Secret = () => { setTitle(validator.unescape(json.title)); } - if (json.file) { - setFile(json.file); + if (json.files) { + setFiles(json.files); } if (json.preventBurn) { @@ -99,12 +99,12 @@ const Secret = () => { setPassword(event.target.value); }; - const onFileDownload = (event) => { + const onFilesDownload = (event) => { event.preventDefault(); downloadFile( { - ...file, + files, encryptionKey, secretId, }, @@ -192,7 +192,7 @@ const Secret = () => { )} - {file && !isDownloaded && ( + {files && !isDownloaded && ( )} diff --git a/src/server/controllers/upload.js b/src/server/controllers/download.js similarity index 88% rename from src/server/controllers/upload.js rename to src/server/controllers/download.js index fcf2427..cdcf69f 100644 --- a/src/server/controllers/upload.js +++ b/src/server/controllers/download.js @@ -3,8 +3,8 @@ import fileAdapter from '../services/file-adapter.js'; import * as redis from '../services/redis.js'; // https://stackabuse.com/uploading-files-to-aws-s3-with-node-js -async function uploadFiles(fastify) { - fastify.post('/download', async (request, reply) => { +async function downloadFiles(fastify) { + fastify.post('/', async (request, reply) => { const { key, encryptionKey, secretId, ext, mime } = request.body; const fileKey = sanitize(key); @@ -31,4 +31,4 @@ async function uploadFiles(fastify) { }); } -export default uploadFiles; +export default downloadFiles; diff --git a/src/server/controllers/secret.js b/src/server/controllers/secret.js index 6e30a55..a2b02e5 100644 --- a/src/server/controllers/secret.js +++ b/src/server/controllers/secret.js @@ -44,8 +44,8 @@ async function getSecretRoute(request, reply) { } } - if (data.file) { - Object.assign(result, { file: JSON.parse(data.file) }); + if (data.files) { + Object.assign(result, { files: JSON.parse(data.files) }); } if (data.title) { @@ -96,7 +96,7 @@ async function secret(fastify) { }, async (req, reply) => { const { text, title, ttl, password, allowedIp, preventBurn, maxViews } = req.body; - const { encryptionKey, secretId, file } = req.secret; + const { encryptionKey, secretId, files } = req.secret; if (Buffer.byteLength(text?.value) > config.get('api.maxTextSize')) { return reply.code(413).send({ @@ -133,8 +133,8 @@ async function secret(fastify) { Object.assign(data, { password: await hash(validator.escape(password.value)) }); } - if (file) { - Object.assign(data, { file }); + if (files) { + Object.assign(data, { files }); } if (preventBurn?.value === 'true') { diff --git a/src/server/decorators/attachment-upload.js b/src/server/decorators/attachment-upload.js index a0cfa08..fb2402e 100644 --- a/src/server/decorators/attachment-upload.js +++ b/src/server/decorators/attachment-upload.js @@ -6,20 +6,31 @@ import fileAdapter from '../services/file-adapter.js'; export default fp(async (fastify) => { fastify.decorate('attachment', async (req, reply) => { - const file = await req.body.file; + const reqFiles = await req.body['files[]']; const { encryptionKey } = req.secret; - if (file.mimetype) { - const fileData = await file.toBuffer(); + const files = (reqFiles?.length ? reqFiles : [reqFiles]).filter(Boolean); - const metadata = await fileTypeFromBuffer(fileData); + if (files?.length) { + req.secret.files = []; - const mime = metadata?.mime ? metadata.mime : file.mimetype.toString(); - const ext = metadata?.ext ? metadata.ext : path.extname(file.filename).replace('.', ''); + // yeah, for loop, I know. Could easily be reduce or what not + for (let i = 0; i < files.length; i++) { + const file = files[i]; - const imageData = await fileAdapter.upload(encryptionKey, fileData); + const fileData = await file.toBuffer(); - Object.assign(req.secret, { file: { ext, mime, key: imageData.key } }); + const metadata = await fileTypeFromBuffer(fileData); + + const mime = metadata?.mime ? metadata.mime : file.mimetype.toString(); + const ext = metadata?.ext + ? metadata.ext + : path.extname(file.filename).replace('.', ''); + + const imageData = await fileAdapter.upload(encryptionKey, fileData); + + req.secret.files.push({ ext, mime, key: imageData.key }); + } } }); }); diff --git a/src/server/services/redis.js b/src/server/services/redis.js index 1e0e223..b420891 100644 --- a/src/server/services/redis.js +++ b/src/server/services/redis.js @@ -43,8 +43,8 @@ export async function createSecret(data, ttl) { prepare.push(...['allowed_ip', data.allowedIp]); } - if (data.file) { - prepare.push(...['file', JSON.stringify(data.file)]); + if (data.files) { + prepare.push(...['files', JSON.stringify(data.files)]); } if (data.preventBurn) {