diff --git a/.env.example b/.env.example index 9a57b1a..128203b 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,17 @@ PORT=3000 # You must update this to the url you use to access your site BASE_URL=http://localhost:3000/ -# Node environment (default: development) -NODE_ENV=development +#ALLOWED_IFRAME_ORIGINS= #DEPRECATED and will be used as ALLOWED_ORIGINS if SET + +# Comma-separated list of allowed origins for CORS +# (default: '*' if empty, add your base_url if you want to restrict only to base_url) +# When adding multiple origins, base_url will be included by default +# ALLOWED_ORIGINS: http://internalip:port,https://subdomain.example.com +ALLOWED_ORIGINS= + +# Node environment (default: production) +# When set to 'development', ALLOWED_ORIGINS will default to '*' +NODE_ENV=production ######################################### # FILE UPLOAD SETTINGS @@ -62,8 +71,4 @@ APPRISE_SIZE_UNIT=Auto ######################################### # Enable automatic upload on file selection (true/false, default: false) -AUTO_UPLOAD=false - -# Comma-separated list of origins allowed to embed the app in an iframe (optional) -# ALLOWED_IFRAME_ORIGINS=https://example.com,https://another.com -ALLOWED_IFRAME_ORIGINS= \ No newline at end of file +AUTO_UPLOAD=false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 04a9654..fa00d48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Base stage for shared configurations -FROM node:20-alpine as base +FROM node:22-alpine as base # Install python and create virtual environment with minimal dependencies RUN apk add --no-cache python3 py3-pip && \ diff --git a/README.md b/README.md index c93f666..4ac223d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A stupid simple file upload application that provides a clean, modern interface No auth (unless you want it now!), no storage, no nothing. Just a simple file uploader to drop dumb files into a dumb folder. ## Table of Contents + - [Quick Start](#quick-start) - [Production Deployment with Docker](#production-deployment-with-docker) - [Local Development (Recommended Quick Start)](LOCAL_DEVELOPMENT.md) @@ -21,44 +22,51 @@ No auth (unless you want it now!), no storage, no nothing. Just a simple file up ## Quick Start ### Option 1: Docker (For Dummies) + ```bash # Pull and run with one command docker run -p 3000:3000 -v ./uploads:/app/uploads dumbwareio/dumbdrop:latest ``` + 1. Go to http://localhost:3000 2. Upload a File - It'll show up in ./uploads 3. Celebrate on how dumb easy this was ### Option 2: Docker Compose (For Dummies who like customizing) + Create a `docker-compose.yml` file: + ```yaml services: - dumbdrop: - image: dumbwareio/dumbdrop:latest - ports: - - 3000:3000 - volumes: - # Where your uploaded files will land - - ./uploads:/app/uploads - environment: - # Explicitly set upload directory inside the container - UPLOAD_DIR: /app/uploads - # The title shown in the web interface - DUMBDROP_TITLE: DumbDrop - # Maximum file size in MB - MAX_FILE_SIZE: 1024 - # Optional PIN protection (leave empty to disable) - DUMBDROP_PIN: 123456 - # Upload without clicking button - AUTO_UPLOAD: false - # The base URL for the application - # You must update this to the url you use to access your site - BASE_URL: http://localhost:3000 + dumbdrop: + image: dumbwareio/dumbdrop:latest + ports: + - 3000:3000 + volumes: + # Where your uploaded files will land + - ./uploads:/app/uploads + environment: + # Explicitly set upload directory inside the container + UPLOAD_DIR: /app/uploads + # The title shown in the web interface + DUMBDROP_TITLE: DumbDrop + # Maximum file size in MB + MAX_FILE_SIZE: 1024 + # Optional PIN protection (leave empty to disable) + DUMBDROP_PIN: 123456 + # Upload without clicking button + AUTO_UPLOAD: false + # The base URL for the application + # You must update this to the url you use to access your site + BASE_URL: http://localhost:3000 ``` + Then run: + ```bash docker compose up -d ``` + 1. Go to http://localhost:3000 2. Upload a File - It'll show up in ./uploads 3. Rejoice in the glory of your dumb uploads @@ -90,21 +98,22 @@ For local development setup, troubleshooting, and advanced usage, see the dedica ### Environment Variables -| Variable | Description | Default | Required | -|------------------------|------------------------------------------------------------------|-----------------------------------------|----------| -| PORT | Server port | 3000 | No | -| BASE_URL | Base URL for the application | http://localhost:PORT | No | -| MAX_FILE_SIZE | Maximum file size in MB | 1024 | No | -| DUMBDROP_PIN | PIN protection (4-10 digits) | None | No | -| DUMBDROP_TITLE | Site title displayed in header | DumbDrop | No | -| APPRISE_URL | Apprise URL for notifications | None | No | -| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No | -| APPRISE_SIZE_UNIT | Size unit for notifications (B, KB, MB, GB, TB, or Auto) | Auto | No | -| AUTO_UPLOAD | Enable automatic upload on file selection | false | No | -| ALLOWED_EXTENSIONS | Comma-separated list of allowed file extensions | None | No | -| ALLOWED_IFRAME_ORIGINS | Comma-separated list of origins allowed to embed the app in an iframe | None | No | -| UPLOAD_DIR | Directory for uploads (Docker/production; should be `/app/uploads` in container) | None (see LOCAL_UPLOAD_DIR fallback) | No | -| LOCAL_UPLOAD_DIR | Directory for uploads (local dev, fallback: './local_uploads') | ./local_uploads | No | +| Variable | Description | Default | Required | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------- | +| PORT | Server port | 3000 | No | +| BASE_URL | Base URL for the application | http://localhost:PORT | No | +| MAX_FILE_SIZE | Maximum file size in MB | 1024 | No | +| DUMBDROP_PIN | PIN protection (4-10 digits) | None | No | +| DUMBDROP_TITLE | Site title displayed in header | DumbDrop | No | +| APPRISE_URL | Apprise URL for notifications | None | No | +| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No | +| APPRISE_SIZE_UNIT | Size unit for notifications (B, KB, MB, GB, TB, or Auto) | Auto | No | +| AUTO_UPLOAD | Enable automatic upload on file selection | false | No | +| ALLOWED_EXTENSIONS | Comma-separated list of allowed file extensions | None | No | +| ALLOWED_IFRAME_ORIGINS (deprecated: see ALLOWED_ORIGINS) | Comma-separated list of origins allowed to embed the app in an iframe | None | No | +| ALLOWED_ORIGINS | You can restrict CORS to your BASE_URL or a comma-separated list of specified origins, which will automatically include your base_url | '\*' | No | +| UPLOAD_DIR | Directory for uploads (Docker/production; should be `/app/uploads` in container) | None (see LOCAL_UPLOAD_DIR fallback) | No | +| LOCAL_UPLOAD_DIR | Directory for uploads (local dev, fallback: './local_uploads') | ./local_uploads | No | - **UPLOAD_DIR** is used in Docker/production. If not set, LOCAL_UPLOAD_DIR is used for local development. If neither is set, the default is `./local_uploads`. - **Docker Note:** The Dockerfile now only creates the `uploads` directory inside the container. The host's `./local_uploads` is mounted to `/app/uploads` and should be managed on the host system. @@ -114,50 +123,76 @@ For local development setup, troubleshooting, and advanced usage, see the dedica See `.env.example` for a template and more details.
-ALLOWED_IFRAME_ORIGINS +ALLOWED_IFRAME_ORIGINS (DEPRECATED: see ALLOWED_ORIGINS) -To allow this app to be embedded in an iframe on specific origins (such as Organizr), set the `ALLOWED_IFRAME_ORIGINS` environment variable. For example: +- This is now deprecated but still works for backwards compatibility +- ALLOWED_IFRAME_ORIGINS will be used as a fallback if ALLOWED_ORIGINS is not set +- Please update to ALLOWED_ORIGINS for future compatibility + +~~To allow this app to be embedded in an iframe on specific origins (such as Organizr), set the `ALLOWED_IFRAME_ORIGINS` environment variable. For example:~~ ```env ALLOWED_IFRAME_ORIGINS=https://organizr.example.com,https://myportal.com ``` -- If not set, the app will only allow itself to be embedded in an iframe on the same origin (default security). -- If set, the app will allow embedding in iframes on the specified origins and itself. -- **Security Note:** Only add trusted origins. Allowing arbitrary origins can expose your app to clickjacking and other attacks. +- ~~If not set, the app will only allow itself to be embedded in an iframe on the same origin (default security).~~ +- ~~If set, the app will allow embedding in iframes on the specified origins and itself.~~ +- ~~**Security Note:** Only add trusted origins. Allowing arbitrary origins can expose your app to clickjacking and other attacks.~~
+
+ALLOWED_ORIGINS + +By default `ALLOWED_ORIGINS` is set to '\*' + +```env +ALLOWED_ORIGINS=https://organizr.example.com,https://myportal.com,http://internalip:port +``` + +- If you would like to restrict CORS to your BASE_URL, you can set it like this: `ALLOWED_ORIGINS=http://localhost:3000` +- If you would like to allow multiple origins, you can set it like this: `ALLOWED_ORIGINS=http://internalip:port,https://subdomain.domain.tld` + - This will automatically include your BASE_URL in the list of allowed origins. +
+
File Extension Filtering To restrict which file types can be uploaded, set the `ALLOWED_EXTENSIONS` environment variable. For example: + ```env ALLOWED_EXTENSIONS=.jpg,.jpeg,.png,.pdf,.doc,.docx,.txt ``` + If not set, all file extensions will be allowed. +
Notification Setup #### Message Templates + The notification message supports the following placeholders: + - `{filename}`: Name of the uploaded file - `{size}`: Size of the file (formatted according to APPRISE_SIZE_UNIT) - `{storage}`: Total size of all files in upload directory Example message template: + ```env APPRISE_MESSAGE: New file uploaded {filename} ({size}), Storage used {storage} ``` Size formatting examples: + - Auto (default): Chooses nearest unit (e.g., "1.44MB", "256KB") - Fixed unit: Set APPRISE_SIZE_UNIT to B, KB, MB, GB, or TB Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UNIT. #### Notification Support + - Integration with [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#supported-notifications) for flexible notifications - Support for all Apprise notification services - Customizable notification messages with filename templating @@ -167,6 +202,7 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN ## Security ### Features + - Variable-length PIN support (4-10 digits) - Constant-time PIN comparison - Input sanitization @@ -178,6 +214,7 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN ## Technical Details ### Stack + - **Backend**: Node.js (>=20.0.0) with Express - **Frontend**: Vanilla JavaScript (ES6+) - **Container**: Docker with multi-stage builds @@ -186,6 +223,7 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN - **Notifications**: Apprise integration ### Dependencies + - express: Web framework - multer: File upload handling - apprise: Notification system @@ -210,8 +248,10 @@ See [Local Development (Recommended Quick Start)](LOCAL_DEVELOPMENT.md) for loca --- + Made with ❤️ by [DumbWare.io](https://dumbware.io) ## Future Features + - Camera Upload for Mobile -> Got an idea? [Open an issue](https://github.com/dumbwareio/dumbdrop/issues) or [submit a PR](https://github.com/dumbwareio/dumbdrop/pulls) + > Got an idea? [Open an issue](https://github.com/dumbwareio/dumbdrop/issues) or [submit a PR](https://github.com/dumbwareio/dumbdrop/pulls) diff --git a/docker-compose.yml b/docker-compose.yml index 2a70e37..cfeee23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,9 @@ services: dumbdrop: image: dumbwareio/dumbdrop:latest + # build: . + container_name: dumbdrop + restart: unless-stopped ports: - 3000:3000 volumes: @@ -15,9 +18,15 @@ services: AUTO_UPLOAD: true # Upload without clicking button BASE_URL: http://localhost:3000 # The base URL for the application, You must update this to the url you use to access your site + # Comma-separated list of allowed origins for CORS + # (default: '*' if empty, replace with your base_url if you want to restrict only to base_url) + # When adding multiple origins, base_url will be included by default and does not need to the list + # ALLOWED_IFRAME_ORIGINS: #DEPRECATED and will be used as ALLOWED_ORIGINS if SET + # ALLOWED_ORIGINS: http://internalip:port,https://subdomain.example.com + # Additional available environment variables (commented out with defaults) # PORT: 3000 # Server port (default: 3000) - # NODE_ENV: production # Node environment (development/production) + # NODE_ENV: production # Node environment (development/production) - when not using production ALLOWED_ORIGINS will be set to '*' by default # DEBUG: false # Debug mode for verbose logging (default: false in production, true in development) # APPRISE_URL: "" # Apprise notification URL for upload notifications (default: none) # APPRISE_MESSAGE: "New file uploaded - {filename} ({size}), Storage used {storage}" # Notification message template with placeholders: {filename}, {size}, {storage} diff --git a/package-lock.json b/package-lock.json index ba76b05..4b0ac54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^7.1.5", - "multer": "^1.4.5-lts.1" + "helmet": "^8.1.0", + "multer": "^1.4.5-lts.1", + "toastify-js": "^1.12.0" }, "devDependencies": { "eslint": "^8.56.0", @@ -188,9 +190,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -356,9 +358,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -625,9 +627,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1423,6 +1425,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1801,6 +1812,7 @@ "version": "1.4.5-lts.2", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -2658,6 +2670,12 @@ "node": ">=8.0" } }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index f3717f5..fbc064e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^7.1.5", - "multer": "^1.4.5-lts.1" + "helmet": "^8.1.0", + "multer": "^1.4.5-lts.1", + "toastify-js": "^1.12.0" }, "devDependencies": { "eslint": "^8.56.0", diff --git a/public/index.html b/public/index.html index af953d6..9998091 100644 --- a/public/index.html +++ b/public/index.html @@ -4,12 +4,11 @@ {{SITE_TITLE}} - Simple File Upload - - - - - - + + + + +
@@ -156,7 +155,7 @@ // Remove leading slash from API path before concatenating const apiUrl = '/api/upload/init'.startsWith('/') ? '/api/upload/init'.substring(1) : '/api/upload/init'; - const response = await fetch(window.BASE_URL + apiUrl, { + const response = await fetch(apiUrl, { method: 'POST', headers, body: JSON.stringify({ @@ -219,7 +218,7 @@ const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30-second timeout per attempt - const response = await fetch(window.BASE_URL + chunkApiUrl, { + const response = await fetch(chunkApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', @@ -395,7 +394,7 @@ const cancelApiUrlPath = `/api/upload/cancel/${this.uploadId}`; const cancelApiUrl = cancelApiUrlPath.startsWith('/') ? cancelApiUrlPath.substring(1) : cancelApiUrlPath; // No need to wait for response here, just fire and forget - fetch(window.BASE_URL + cancelApiUrl, { method: 'POST' }).catch(err => { + fetch(cancelApiUrl, { method: 'POST' }).catch(err => { console.warn(`Sending cancel request failed for upload ${this.uploadId}:`, err); }); } catch (cancelError) { diff --git a/public/login.html b/public/login.html index c5fc37a..798aa72 100644 --- a/public/login.html +++ b/public/login.html @@ -4,8 +4,8 @@ {{SITE_TITLE}} - Login - - + + -