8 Commits

Author SHA1 Message Date
Chris
1a8fe19416 Fix/cors csp (#64)
* CORS/CSP fix

* deprecate ALLOWED_IFRAME_ORIGINS

* Revert "deprecate ALLOWED_IFRAME_ORIGINS"

This reverts commit 9792f06691.

* Reapply "deprecate ALLOWED_IFRAME_ORIGINS"

This reverts commit 683ee93036.

* Add helmet config and deprecate previous ALLOWED_IFRAME_ORIGINS

* add build to docker compose for local builds

* set server to listen on 0.0.0.0 and control with cors

* Remove hsts from helmet and apply new pin status check limits

* add back allowed_iframe_origins env as a fallback for allowed_origins

* update readme for allowed_iframe_origins
2025-06-20 15:07:56 -07:00
abite
54cdf4be36 Update README.md 2025-06-04 11:07:48 -05:00
abite
db27b25372 Merge pull request #56 from gitmotion/fix/escape-html-xss
Add html escaping to frontend uploader for xss security
2025-05-15 12:54:35 -05:00
abite
478477c6ea Merge pull request #57 from gitmotion/update-icon
Update icons to selfh.st icons
2025-05-13 11:15:40 -04:00
gitmotion
d37760e9dd Update icons to selfh.st icons 2025-05-13 08:10:47 -07:00
gitmotion
1835f611da Add html escaping to frontend uploader for xss security
replace innerhtml to textcontent
2025-05-12 13:31:58 -07:00
greirson
5177752a6a chore: Update comments in environment configuration files to clarify BASE_URL usage 2025-05-09 07:59:14 -07:00
Greirson Lee-Thorp
c75d200c70 Update docker-publish.yml to support :dev tag 2025-05-05 16:53:40 -07:00
18 changed files with 423 additions and 194 deletions

View File

@@ -5,11 +5,21 @@
# Port for the server (default: 3000)
PORT=3000
# Base URL for the application (default: http://localhost:PORT)
# Base URL for the application (default: http://localhost:PORT) -
# 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
@@ -61,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=
AUTO_UPLOAD=false

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main # Trigger the workflow on pushes to the main branch
- dev # Trigger the workflow on pushes to the dev branch
jobs:
build-and-push:
@@ -39,6 +40,8 @@ jobs:
images: |
name=dumbwareio/dumbdrop
tags: |
# Add :dev tag for pushes to the dev branch
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
# the semantic versioning tags add "latest" when a version tag is present
# but since version tags aren't being used (yet?) let's add "latest" anyway
type=raw,value=latest

View File

@@ -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 && \

129
README.md
View File

@@ -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,43 +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
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
@@ -89,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.
@@ -113,50 +123,76 @@ For local development setup, troubleshooting, and advanced usage, see the dedica
See `.env.example` for a template and more details.
<details>
<summary>ALLOWED_IFRAME_ORIGINS</summary>
<summary>ALLOWED_IFRAME_ORIGINS (DEPRECATED: see ALLOWED_ORIGINS)</summary>
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.~~
</details>
<details>
<summary>ALLOWED_ORIGINS</summary>
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.
</details>
<details>
<summary>File Extension Filtering</summary>
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.
</details>
<details>
<summary>Notification Setup</summary>
#### 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
@@ -166,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
@@ -177,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
@@ -185,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
@@ -202,9 +241,17 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN
See [Local Development (Recommended Quick Start)](LOCAL_DEVELOPMENT.md) for local setup and guidelines.
## Support the Project
<a href="https://www.buymeacoffee.com/dumbware" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="60">
</a>
---
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)

View File

@@ -1,6 +1,9 @@
services:
dumbdrop:
image: dumbwareio/dumbdrop:latest
# build: .
container_name: dumbdrop
restart: unless-stopped
ports:
- 3000:3000
volumes:
@@ -13,11 +16,17 @@ services:
MAX_FILE_SIZE: 1024 # Maximum file size in MB
DUMBDROP_PIN: 123456 # Optional PIN protection (4-10 digits, leave empty to disable)
AUTO_UPLOAD: true # Upload without clicking button
BASE_URL: http://localhost:3000 # The base URL for the application
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}

50
package-lock.json generated
View File

@@ -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",
@@ -1871,9 +1883,9 @@
}
},
"node_modules/nodemon/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -2542,9 +2554,9 @@
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -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",

View File

@@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,14 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect width="256" height="256" rx="32" fill="#4CAF50"/>
<!-- File outline -->
<path d="M76 56C76 47.1634 83.1634 40 92 40H140L180 80V200C180 208.837 172.837 216 164 216H92C83.1634 216 76 208.837 76 200V56Z" fill="white"/>
<!-- Folded corner -->
<path d="M140 40L180 80H148C143.582 80 140 76.4183 140 72V40Z" fill="#E8E8E8"/>
<!-- Arrow -->
<path d="M128 96L96 128H116V168H140V128H160L128 96Z" fill="#4CAF50"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><circle cx="256" cy="256" r="232.7" style="opacity:.2;fill:#487bb7"/><path d="M256 512C114.8 512 0 397.2 0 256S114.8 0 256 0s256 114.8 256 256-114.8 256-256 256m0-465.4c-115.5 0-209.5 94-209.5 209.5s94 209.5 209.5 209.5 209.5-94 209.5-209.5c0-115.6-94-209.5-209.5-209.5M175.9 353H336c3.3 0 5.9 2.9 5.9 6.5V377c0 3.6-2.6 6.5-5.9 6.5H175.9c-3.3 0-5.9-2.9-5.9-6.5v-17.5c0-3.5 2.7-6.5 5.9-6.5m75.3-238.6-79.3 81.1c-4.1 4.2-1.1 11.3 4.8 11.3h36.9c3.7 0 6.7 3 6.7 6.7v108.4c0 3.7 3 6.7 6.7 6.7h58c3.7 0 6.7-3 6.7-6.7V213.5c0-3.7 3-6.7 6.7-6.7h36.9c5.9 0 8.9-7.1 4.8-11.3l-79.3-81.1c-2.6-2.6-7-2.6-9.6 0" style="fill:#487bb7"/></svg>

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 709 B

View File

@@ -4,12 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{SITE_TITLE}} - Simple File Upload</title>
<link rel="stylesheet" href="{{BASE_URL}}styles.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<link rel="manifest" href="{{BASE_URL}}manifest.json">
<link rel="icon" type="image/svg+xml" href="{{BASE_URL}}assets/icon.svg">
<script>window.BASE_URL = '{{BASE_URL}}';</script>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="toastify/toastify.css">
<script src="toastify/toastify.js"></script>
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/svg+xml" href="assets/icon.svg">
</head>
<body>
<div class="container">
@@ -88,6 +87,17 @@
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Security helper to escape HTML to prevent XSS
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
class FileUploader {
constructor(file, batchId) {
this.file = file;
@@ -145,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({
@@ -208,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',
@@ -288,7 +298,8 @@
const label = document.createElement('div');
label.className = 'progress-label';
label.textContent = this.file.webkitRelativePath || this.file.name;
const fileName = this.file.webkitRelativePath || this.file.name;
label.textContent = escapeHtml(fileName);
const progress = document.createElement('div');
progress.className = 'progress';
@@ -383,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) {
@@ -809,7 +820,7 @@
return relativePath.split('/').length === 1;
}).length;
folderItem.innerHTML = `📁 ${folder.name}/ (${formatFileSize(folder.size)} - ${totalFiles} files)`;
folderItem.textContent = `📁 ${escapeHtml(folder.name)}/ (${formatFileSize(folder.size)} - ${totalFiles} files)`;
// Add files in folder
const filesList = document.createElement('div');
@@ -824,7 +835,7 @@
const fileItem = document.createElement('div');
fileItem.className = 'file-item nested';
const relativePath = file.webkitRelativePath.substring(folder.name.length + 1);
fileItem.innerHTML = `📄 ${relativePath} (${formatFileSize(file.size)})`;
fileItem.textContent = `📄 ${escapeHtml(relativePath)} (${formatFileSize(file.size)})`;
filesList.appendChild(fileItem);
});
@@ -846,7 +857,7 @@
.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `📄 ${file.name} (${formatFileSize(file.size)})`;
fileItem.textContent = `📄 ${escapeHtml(file.name)} (${formatFileSize(file.size)})`;
fileList.appendChild(fileItem);
});

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{SITE_TITLE}} - Login</title>
<link rel="stylesheet" href="{{BASE_URL}}styles.css">
<link rel="icon" type="image/svg+xml" href="{{BASE_URL}}assets/icon.svg">
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/svg+xml" href="assets/icon.svg">
<style>
.login-container {
display: flex;
@@ -54,7 +54,6 @@
background-color: var(--textarea-bg);
}
</style>
<script>window.BASE_URL = '{{BASE_URL}}';</script>
</head>
<body>
<div class="login-container">
@@ -126,10 +125,12 @@
// Handle form submission
const verifyPin = async (pin) => {
try {
const response = await fetch(window.BASE_URL + 'api/auth/verify-pin', {
const response = await fetch('/api/auth/verify-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin })
body: JSON.stringify({ pin }),
credentials: 'include', // Ensure cookies are sent
// redirect: 'follow' // Follow server redirects
});
const data = await response.json();
@@ -212,7 +213,7 @@
};
// Check PIN length and initialize
fetch(window.BASE_URL + 'api/auth/pin-required')
fetch('/api/auth/pin-required')
.then(response => {
if (response.status === 429) {
throw new Error('Too many attempts. Please wait before trying again.');
@@ -241,17 +242,6 @@
pinContainer.style.pointerEvents = 'none';
}
});
document.addEventListener('DOMContentLoaded', function() {
// Rewrite asset URLs to use BASE_URL as prefix if not absolute
const baseUrl = window.BASE_URL;
document.querySelectorAll('link[rel="stylesheet"], link[rel="icon"]').forEach(link => {
const href = link.getAttribute('href');
if (href && !href.startsWith('http') && !href.startsWith('data:') && !href.startsWith(baseUrl)) {
link.setAttribute('href', baseUrl + href.replace(/^\//, ''));
}
});
});
</script>
</body>
</html>

View File

@@ -6,6 +6,7 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');
@@ -14,32 +15,66 @@ const fsPromises = require('fs').promises;
const { config, validateConfig } = require('./config');
const logger = require('./utils/logger');
const { ensureDirectoryExists } = require('./utils/fileUtils');
const { securityHeaders, requirePin } = require('./middleware/security');
const { getHelmetConfig, requirePin } = require('./middleware/security');
const { safeCompare } = require('./utils/security');
const { initUploadLimiter, pinVerifyLimiter, downloadLimiter } = require('./middleware/rateLimiter');
const { initUploadLimiter, pinVerifyLimiter, pinStatusLimiter, downloadLimiter } = require('./middleware/rateLimiter');
const { injectDemoBanner, demoMiddleware } = require('./utils/demoMode');
const { originValidationMiddleware, getCorsOptions } = require('./middleware/cors');
// Create Express app
const app = express();
const PORT = process.env.PORT || 3000;
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
// Add this line to trust the first proxy
app.set('trust proxy', 1);
// Middleware setup
app.use(cors());
app.use(cors(getCorsOptions(BASE_URL)));
app.use(cookieParser());
app.use(express.json());
app.use(securityHeaders);
app.use(helmet(getHelmetConfig()));
// --- AUTHENTICATION MIDDLEWARE FOR ALL PROTECTED ROUTES ---
app.use((req, res, next) => {
// List of paths that should be publicly accessible
const publicPaths = [
'/login',
'/login.html',
'/api/auth/logout',
'/api/auth/verify-pin',
'/api/auth/pin-required',
'/api/auth/pin-length',
'/pin-length',
'/verify-pin',
'/config.js',
'/assets/',
'/styles.css',
'/manifest.json',
'/asset-manifest.json',
'/toastify',
];
// Check if the current path matches any of the public paths
if (publicPaths.some(path => req.path.startsWith(path))) {
return next();
}
// For all other paths, apply both origin validation and auth middleware
originValidationMiddleware(req, res, () => {
demoMiddleware(req, res, next);
});
});
// Import routes
const { router: uploadRouter } = require('./routes/upload');
const fileRoutes = require('./routes/files');
const authRoutes = require('./routes/auth');
// Add demo middleware before your routes
app.use(demoMiddleware);
// Use routes with appropriate middleware
// Apply strict rate limiting to PIN verification, but more permissive to status checks
app.use('/api/auth/pin-required', pinStatusLimiter);
app.use('/api/auth/logout', pinStatusLimiter);
app.use('/api/auth', pinVerifyLimiter, authRoutes);
app.use('/api/upload', requirePin(config.pin), initUploadLimiter, uploadRouter);
app.use('/api/files', requirePin(config.pin), downloadLimiter, fileRoutes);
@@ -55,9 +90,6 @@ app.get('/', (req, res) => {
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
html = html.replace('{{MAX_RETRIES}}', config.clientMaxRetries.toString());
// Ensure baseUrl has a trailing slash for correct asset linking
const baseUrlWithSlash = config.baseUrl.endsWith('/') ? config.baseUrl : config.baseUrl + '/';
html = html.replace(/{{BASE_URL}}/g, baseUrlWithSlash);
html = injectDemoBanner(html);
res.send(html);
});
@@ -71,9 +103,6 @@ app.get('/login.html', (req, res) => {
let html = fs.readFileSync(path.join(__dirname, '../public', 'login.html'), 'utf8');
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
// Ensure baseUrl has a trailing slash
const baseUrlWithSlash = config.baseUrl.endsWith('/') ? config.baseUrl : config.baseUrl + '/';
html = html.replace(/{{BASE_URL}}/g, baseUrlWithSlash);
html = injectDemoBanner(html);
res.send(html);
});
@@ -104,6 +133,8 @@ app.use((req, res, next) => {
// Serve remaining static files
app.use(express.static('public'));
// Serve Toastify assets under /toastify
app.use('/toastify', express.static(path.join(__dirname, '../node_modules/toastify-js/src')));
// Error handling middleware
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars

View File

@@ -1,16 +1,5 @@
require('dotenv').config();
console.log('Loaded ENV:', {
PORT: process.env.PORT,
UPLOAD_DIR: process.env.UPLOAD_DIR,
LOCAL_UPLOAD_DIR: process.env.LOCAL_UPLOAD_DIR,
NODE_ENV: process.env.NODE_ENV
});
console.log('Loaded ENV:', {
PORT: process.env.PORT,
UPLOAD_DIR: process.env.UPLOAD_DIR,
LOCAL_UPLOAD_DIR: process.env.LOCAL_UPLOAD_DIR,
NODE_ENV: process.env.NODE_ENV
});
const { validatePin } = require('../utils/security');
const logger = require('../utils/logger');
const fs = require('fs');
@@ -33,7 +22,6 @@ const { version } = require('../../package.json'); // Get version from package.j
* APPRISE_MESSAGE - Notification message template (default provided)
* APPRISE_SIZE_UNIT - Size unit for notifications (optional)
* ALLOWED_EXTENSIONS - Comma-separated list of allowed file extensions (optional)
* ALLOWED_IFRAME_ORIGINS - Comma-separated list of allowed iframe origins (optional)
*/
// Helper for clear configuration logging
@@ -43,12 +31,20 @@ const logConfig = (message, level = 'info') => {
};
// Default configurations
const DEFAULT_PORT = 3000;
const DEFAULT_CHUNK_SIZE = 1024 * 1024 * 100; // 100MB
const DEFAULT_SITE_TITLE = 'DumbDrop';
const DEFAULT_BASE_URL = 'http://localhost:3000';
const NODE_ENV = process.env.NODE_ENV || 'production';
const PORT = process.env.PORT || 3000;
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
const DEFAULT_CLIENT_MAX_RETRIES = 5; // Default retry count
console.log('Loaded ENV:', {
PORT,
UPLOAD_DIR: process.env.UPLOAD_DIR,
LOCAL_UPLOAD_DIR: process.env.LOCAL_UPLOAD_DIR,
NODE_ENV,
BASE_URL,
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*',
});
const logAndReturn = (key, value, isDefault = false) => {
logConfig(`${key}: ${value}${isDefault ? ' (default)' : ''}`);
return value;
@@ -83,7 +79,7 @@ function determineUploadDirectory() {
* Returns true if NODE_ENV is not 'production' and UPLOAD_DIR is not set (i.e., not Docker)
*/
function isLocalDevelopment() {
return process.env.NODE_ENV !== 'production' && !process.env.UPLOAD_DIR;
return process.env.NODE_ENV !== 'production';
}
/**
@@ -121,17 +117,17 @@ const config = {
* Port for the server (default: 3000)
* Set via PORT in .env
*/
port: process.env.PORT || DEFAULT_PORT,
port: PORT,
/**
* Node environment (default: 'development')
* Node environment (default: 'production')
* Set via NODE_ENV in .env
*/
nodeEnv: process.env.NODE_ENV || 'development',
nodeEnv: NODE_ENV,
/**
* Base URL for the app (default: http://localhost:${PORT})
* Set via BASE_URL in .env
*/
baseUrl: process.env.BASE_URL || DEFAULT_BASE_URL,
baseUrl: BASE_URL,
// =====================
// =====================
@@ -211,10 +207,6 @@ const config = {
process.env.ALLOWED_EXTENSIONS.split(',').map(ext => ext.trim().toLowerCase()) :
null,
allowedIframeOrigins: process.env.ALLOWED_IFRAME_ORIGINS
? process.env.ALLOWED_IFRAME_ORIGINS.split(',').map(origin => origin.trim()).filter(Boolean)
: null,
/**
* Max number of retries for client-side chunk uploads (default: 5)
* Set via CLIENT_MAX_RETRIES in .env
@@ -251,7 +243,6 @@ function validateConfig() {
// Validate BASE_URL format
try {
let url = new URL(config.baseUrl);
// Ensure BASE_URL ends with a slash
if (!config.baseUrl.endsWith('/')) {
logger.warn('BASE_URL did not end with a trailing slash. Automatically appending "/".');

84
src/middleware/cors.js Normal file
View File

@@ -0,0 +1,84 @@
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS || process.env.ALLOWED_IFRAME_ORIGINS || '*';
const NODE_ENV = process.env.NODE_ENV || 'production';
let allowedOrigins = [];
function setupOrigins(baseUrl) {
const normalizedBaseUrl = normalizeOrigin(baseUrl);
allowedOrigins = [ normalizedBaseUrl ];
if (NODE_ENV === 'development' || ALLOWED_ORIGINS === '*') allowedOrigins = '*';
else if (ALLOWED_ORIGINS && typeof ALLOWED_ORIGINS === 'string') {
try {
const allowed = ALLOWED_ORIGINS.split(',').map(origin => origin.trim());
allowed.forEach(origin => {
const normalizedOrigin = normalizeOrigin(origin);
if (normalizedOrigin !== baseUrl) allowedOrigins.push(normalizedOrigin);
});
}
catch (error) {
console.error(`Error setting up ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}:`, error);
}
}
console.log("ALLOWED ORIGINS:", allowedOrigins);
return allowedOrigins;
}
function normalizeOrigin(origin) {
if (origin) {
try {
const normalizedOrigin = new URL(origin).origin;
return normalizedOrigin;
} catch (error) {
console.error("Error parsing referer URL:", error);
throw new Error("Error parsing referer URL:", error);
}
}
}
function validateOrigin(origin) {
if (NODE_ENV === 'development' || allowedOrigins === '*') return true;
try {
if (origin) origin = normalizeOrigin(origin);
else {
console.warn("No origin to validate.");
return false;
}
console.log("Validating Origin:", origin);
if (allowedOrigins.includes(origin)) {
console.log("Allowed request from origin:", origin);
return true;
}
else {
console.warn("Blocked request from origin:", origin);
return false;
}
}
catch (error) {
console.error(error);
}
}
function originValidationMiddleware(req, res, next) {
const origin = req.headers.origin || req.headers.referer || `${req.protocol}://${req.headers.host}`;
const isOriginValid = validateOrigin(origin);
if (isOriginValid) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
}
function getCorsOptions(baseUrl) {
const allowedOrigins = setupOrigins(baseUrl);
const corsOptions = {
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Pin', 'X-Batch-Id'],
};
return corsOptions;
}
module.exports = { getCorsOptions, originValidationMiddleware, validateOrigin, allowedOrigins };

View File

@@ -48,7 +48,7 @@ const chunkUploadLimiter = createLimiter({
/**
* Rate limiter for PIN verification attempts
* Prevents brute force attacks
* Prevents brute force attacks on actual PIN verification
*/
const pinVerifyLimiter = createLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
@@ -57,6 +57,24 @@ const pinVerifyLimiter = createLimiter({
error: 'Too many PIN verification attempts. Please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
// Apply strict rate limiting only to PIN verification, not PIN status checks
skip: (req) => {
return req.path === '/pin-required'; // Skip rate limiting for PIN requirement checks
}
});
/**
* Rate limiter for PIN status checks
* More permissive for checking if PIN is required
*/
const pinStatusLimiter = createLimiter({
windowMs: 60 * 1000, // 1 minute window
max: 30, // 30 requests per minute
message: {
error: 'Too many requests. Please wait before trying again.'
},
standardHeaders: true,
legacyHeaders: false
});
@@ -78,5 +96,6 @@ module.exports = {
initUploadLimiter,
chunkUploadLimiter,
pinVerifyLimiter,
pinStatusLimiter,
downloadLimiter
};

View File

@@ -6,41 +6,67 @@
const { safeCompare } = require('../utils/security');
const logger = require('../utils/logger');
const { config } = require('../config');
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'production';
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
// const { config } = require('../config');
/**
* Security headers middleware
* DEPRECATED: Use helmet middleware instead for security headers
*/
function securityHeaders(req, res, next) {
// Content Security Policy
let csp =
"default-src 'self'; " +
"connect-src 'self'; " +
"style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
"script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
"img-src 'self' data: blob:;";
// function securityHeaders(req, res, next) {
// // Content Security Policy
// let csp =
// "default-src 'self'; " +
// "connect-src 'self'; " +
// "style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
// "script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
// "img-src 'self' data: blob:;";
// If allowedIframeOrigins is set, allow those origins to embed via iframe
if (config.allowedIframeOrigins && config.allowedIframeOrigins.length > 0) {
// Remove X-Frame-Options header (do not set it)
// Add frame-ancestors directive to CSP
const frameAncestors = ["'self'", ...config.allowedIframeOrigins].join(' ');
csp += ` frame-ancestors ${frameAncestors};`;
} else {
// Default: only allow same origin if not configured
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
}
// // If allowedIframeOrigins is set, allow those origins to embed via iframe
// if (config.allowedIframeOrigins && config.allowedIframeOrigins.length > 0) {
// // Remove X-Frame-Options header (do not set it)
// // Add frame-ancestors directive to CSP
// const frameAncestors = ["'self'", ...config.allowedIframeOrigins].join(' ');
// csp += ` frame-ancestors ${frameAncestors};`;
// } else {
// // Default: only allow same origin if not configured
// res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// }
res.setHeader('Content-Security-Policy', csp);
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
// res.setHeader('Content-Security-Policy', csp);
// res.setHeader('X-Content-Type-Options', 'nosniff');
// res.setHeader('X-XSS-Protection', '1; mode=block');
// Strict Transport Security (when in production)
if (process.env.NODE_ENV === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
// // Strict Transport Security (when in production)
// if (process.env.NODE_ENV === 'production') {
// res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// }
next();
// next();
// }
function getHelmetConfig() {
// const isSecure = BASE_URL.startsWith('https://');
return {
noSniff: true, // Prevent MIME type sniffing
frameguard: { action: 'deny' }, // Prevent clickjacking
crossOriginEmbedderPolicy: false, // Disable for local network access
crossOriginOpenerPolicy: false, // Disable to prevent warnings on HTTP
crossOriginResourcePolicy: { policy: 'cross-origin' }, // Allow cross-origin for local network
referrerPolicy: { policy: 'no-referrer-when-downgrade' }, // Set referrer policy
ieNoOpen: true, // Prevent IE from executing downloads
// hsts: isSecure ? { maxAge: 31536000, includeSubDomains: true } : false, // Only enforce HTTPS if using HTTPS
// Disabled Helmet middlewares:
hsts: false,
contentSecurityPolicy: false, // Disable CSP for now
dnsPrefetchControl: true, // Disable DNS prefetching
permittedCrossDomainPolicies: false,
originAgentCluster: false,
xssFilter: false,
};
}
/**
@@ -66,7 +92,7 @@ function requirePin(PIN) {
// Set cookie for subsequent requests with enhanced security
const cookieOptions = {
httpOnly: true, // Always enable HttpOnly
secure: req.secure || req.headers['x-forwarded-proto'] === 'https', // Enable secure flag only if the request is over HTTPS
secure: req.secure || (BASE_URL.startsWith('https') && NODE_ENV === 'production'),
sameSite: 'strict',
path: '/',
maxAge: 24 * 60 * 60 * 1000 // 24 hour expiry
@@ -82,6 +108,7 @@ function requirePin(PIN) {
}
module.exports = {
securityHeaders,
// securityHeaders, // Deprecated, use helmet instead
getHelmetConfig,
requirePin
};

View File

@@ -11,7 +11,9 @@ const {
MAX_ATTEMPTS,
LOCKOUT_DURATION
} = require('../utils/security');
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'production';
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
/**
* Verify PIN
*/
@@ -22,13 +24,14 @@ router.post('/verify-pin', (req, res) => {
try {
// If no PIN is set in config, always return success
if (!config.pin) {
res.cookie('DUMBDROP_PIN', '', {
httpOnly: true,
secure: req.secure || (process.env.NODE_ENV === 'production' && config.baseUrl.startsWith('https')),
sameSite: 'strict',
path: '/'
});
return res.json({ success: true, error: null });
// res.cookie('DUMBDROP_PIN', '', {
// httpOnly: true,
// secure: req.secure || (BASE_URL.startsWith('https') && NODE_ENV === 'production'),
// sameSite: 'strict',
// path: '/'
// });
res.clearCookie('DUMBDROP_PIN', { path: '/' });
return res.json({ success: true, error: null, path: '/' });
}
// Validate PIN format
@@ -63,7 +66,7 @@ router.post('/verify-pin', (req, res) => {
// Set secure cookie with cleaned PIN
res.cookie('DUMBDROP_PIN', cleanedPin, {
httpOnly: true,
secure: req.secure || (process.env.NODE_ENV === 'production' && config.baseUrl.startsWith('https')),
secure: req.secure || (BASE_URL.startsWith('https') && NODE_ENV === 'production'),
sameSite: 'strict',
path: '/'
});

View File

@@ -22,10 +22,11 @@ async function startServer() {
// Initialize the application
await initialize();
// Start the server
const server = app.listen(config.port, () => {
// Start the server - bind to 0.0.0.0 for Docker compatibility
const server = app.listen(config.port, '0.0.0.0', () => {
logger.info(`Server running at ${config.baseUrl}`);
logger.info(`Upload directory: ${config.uploadDisplayPath}`);
logger.info(`Server listening on 0.0.0.0:${config.port}`);
logger.info(`Upload directory: ${config.uploadDir}`);
// List directory contents in development
if (config.nodeEnv === 'development') {