mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-11-14 19:05:38 +00:00
Add simple auth
Added Pin verification, set as DUMBDROP_PIN env variable.
This commit is contained in:
20
README.md
20
README.md
@@ -17,6 +17,7 @@ No auth, no storage, no nothing. Just a simple file uploader to drop dumb files
|
|||||||
- Dark Mode toggle
|
- Dark Mode toggle
|
||||||
- Configurable file size limits
|
- Configurable file size limits
|
||||||
- Drag and Drop Directory Support (Maintains file structure in upload)
|
- Drag and Drop Directory Support (Maintains file structure in upload)
|
||||||
|
- Optional PIN protection
|
||||||
|
|
||||||
# Future Features
|
# Future Features
|
||||||
- Camera Upload for Mobile
|
- Camera Upload for Mobile
|
||||||
@@ -36,6 +37,7 @@ npm install
|
|||||||
```env
|
```env
|
||||||
PORT=3000 # Port to run the server on
|
PORT=3000 # Port to run the server on
|
||||||
MAX_FILE_SIZE=1024 # Maximum file size in MB (default: 1024 MB / 1 GB)
|
MAX_FILE_SIZE=1024 # Maximum file size in MB (default: 1024 MB / 1 GB)
|
||||||
|
DUMBDROP_PIN=1234 # Optional PIN protection (leave empty to disable)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the server:
|
3. Start the server:
|
||||||
@@ -52,10 +54,10 @@ docker pull abite3/dumbdrop:latest
|
|||||||
|
|
||||||
# Run the container
|
# Run the container
|
||||||
# For Linux/Mac:
|
# For Linux/Mac:
|
||||||
docker run -p 3000:3000 -v $(pwd)/local_uploads:/uploads abite3/dumbdrop:latest
|
docker run -p 3000:3000 -v $(pwd)/local_uploads:/uploads -e DUMBDROP_PIN=1234 abite3/dumbdrop:latest
|
||||||
|
|
||||||
# For Windows PowerShell:
|
# For Windows PowerShell:
|
||||||
docker run -p 3000:3000 -v "${PWD}\local_uploads:/uploads" abite3/dumbdrop:latest
|
docker run -p 3000:3000 -v "${PWD}\local_uploads:/uploads" -e DUMBDROP_PIN=1234 abite3/dumbdrop:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Build Locally
|
#### Build Locally
|
||||||
@@ -67,19 +69,20 @@ docker build -t dumbdrop .
|
|||||||
2. Run the container:
|
2. Run the container:
|
||||||
```bash
|
```bash
|
||||||
# For Linux/Mac:
|
# For Linux/Mac:
|
||||||
docker run -p 3000:3000 -v $(pwd)/local_uploads:/uploads dumbdrop
|
docker run -p 3000:3000 -v $(pwd)/local_uploads:/uploads -e DUMBDROP_PIN=1234 dumbdrop
|
||||||
|
|
||||||
# For Windows PowerShell:
|
# For Windows PowerShell:
|
||||||
docker run -p 3000:3000 -v "${PWD}\local_uploads:/uploads" dumbdrop
|
docker run -p 3000:3000 -v "${PWD}\local_uploads:/uploads" -e DUMBDROP_PIN=1234 dumbdrop
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Open your browser and navigate to `http://localhost:3000` (unless another domain has been setup)
|
1. Open your browser and navigate to `http://localhost:3000` (unless another domain has been setup)
|
||||||
2. Drag and drop files into the upload area or click "Browse Files"
|
2. If PIN protection is enabled, enter the 4-digit PIN
|
||||||
3. Select one or multiple files
|
3. Drag and drop files into the upload area or click "Browse Files"
|
||||||
4. Click "Upload Files"
|
4. Select one or multiple files
|
||||||
5. Files will be saved to:
|
5. Click "Upload Files"
|
||||||
|
6. Files will be saved to:
|
||||||
- Local development: `./uploads` directory
|
- Local development: `./uploads` directory
|
||||||
- Docker/Unraid: The directory you mapped to `/uploads` in the container
|
- Docker/Unraid: The directory you mapped to `/uploads` in the container
|
||||||
|
|
||||||
@@ -88,4 +91,5 @@ docker run -p 3000:3000 -v "${PWD}\local_uploads:/uploads" dumbdrop
|
|||||||
- Backend: Node.js with Express
|
- Backend: Node.js with Express
|
||||||
- Frontend: Vanilla JavaScript with modern drag-and-drop API
|
- Frontend: Vanilla JavaScript with modern drag-and-drop API
|
||||||
- File handling: Chunked file uploads with configurable size limits
|
- File handling: Chunked file uploads with configurable size limits
|
||||||
|
- Security: Optional PIN protection for uploads
|
||||||
- Containerization: Docker with automated builds via GitHub Actions
|
- Containerization: Docker with automated builds via GitHub Actions
|
||||||
|
|||||||
@@ -9,6 +9,25 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="pin-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="pin-header">
|
||||||
|
<h1>DumbDrop</h1>
|
||||||
|
<h2>Enter PIN</h2>
|
||||||
|
</div>
|
||||||
|
<div class="pin-container">
|
||||||
|
<input type="text" class="pin-digit" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off">
|
||||||
|
<input type="text" class="pin-digit" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off">
|
||||||
|
<input type="text" class="pin-digit" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off">
|
||||||
|
<input type="text" class="pin-digit" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<p id="pin-error" class="error-message"></p>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button id="pin-submit" class="primary">Verify PIN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="theme-toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="theme-toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -412,6 +431,147 @@
|
|||||||
const savedTheme = localStorage.getItem('theme') ||
|
const savedTheme = localStorage.getItem('theme') ||
|
||||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||||
setTheme(savedTheme);
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
let verifiedPin = null;
|
||||||
|
let pinDigits = null; // Declare pinDigits at a higher scope
|
||||||
|
|
||||||
|
// Check if PIN is required
|
||||||
|
const checkPinRequired = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/pin-required');
|
||||||
|
const { required } = await response.json();
|
||||||
|
if (required) {
|
||||||
|
document.getElementById('pin-modal').classList.add('visible');
|
||||||
|
setupPinHandling();
|
||||||
|
} else {
|
||||||
|
initializeApp();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error checking PIN requirement:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle PIN input
|
||||||
|
const handlePinInput = (e, index) => {
|
||||||
|
const input = e.target;
|
||||||
|
const value = input.value;
|
||||||
|
|
||||||
|
// Only allow numbers
|
||||||
|
if (!/^\d*$/.test(value)) {
|
||||||
|
input.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
input.classList.add('filled');
|
||||||
|
// Move to next input if available
|
||||||
|
if (index < pinDigits.length - 1) {
|
||||||
|
pinDigits[index + 1].focus();
|
||||||
|
} else {
|
||||||
|
// If this is the last digit, submit automatically
|
||||||
|
const pin = Array.from(pinDigits).map(digit => digit.value).join('');
|
||||||
|
if (pin.length === 4) {
|
||||||
|
verifyPin(pin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.classList.remove('filled');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify PIN
|
||||||
|
const verifyPin = async (pin) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/verify-pin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ pin })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
verifiedPin = pin;
|
||||||
|
document.getElementById('pin-modal').classList.remove('visible');
|
||||||
|
initializeApp();
|
||||||
|
} else {
|
||||||
|
document.getElementById('pin-error').textContent = 'Invalid PIN';
|
||||||
|
pinDigits.forEach(digit => {
|
||||||
|
digit.value = '';
|
||||||
|
digit.classList.remove('filled');
|
||||||
|
});
|
||||||
|
pinDigits[0].focus();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error verifying PIN:', err);
|
||||||
|
document.getElementById('pin-error').textContent = 'Error verifying PIN';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup PIN handling
|
||||||
|
const setupPinHandling = () => {
|
||||||
|
pinDigits = Array.from(document.querySelectorAll('.pin-digit')); // Assign to the outer variable
|
||||||
|
const submitButton = document.getElementById('pin-submit');
|
||||||
|
|
||||||
|
pinDigits.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => handlePinInput(e, index));
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !input.value && index > 0) {
|
||||||
|
pinDigits[index - 1].focus();
|
||||||
|
pinDigits[index - 1].value = '';
|
||||||
|
pinDigits[index - 1].classList.remove('filled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Handle paste event
|
||||||
|
input.addEventListener('paste', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 4);
|
||||||
|
if (pastedData.length > 0) {
|
||||||
|
pastedData.split('').forEach((digit, i) => {
|
||||||
|
if (i < pinDigits.length) {
|
||||||
|
pinDigits[i].value = digit;
|
||||||
|
pinDigits[i].classList.add('filled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pastedData.length === 4) {
|
||||||
|
verifyPin(pastedData);
|
||||||
|
} else if (pastedData.length < 4) {
|
||||||
|
pinDigits[pastedData.length].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
submitButton.addEventListener('click', () => {
|
||||||
|
const pin = pinDigits.map(digit => digit.value).join('');
|
||||||
|
if (pin.length === 4) {
|
||||||
|
verifyPin(pin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus first input
|
||||||
|
pinDigits[0].focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add PIN to all API requests
|
||||||
|
const fetchWithPin = async (url, options = {}) => {
|
||||||
|
if (verifiedPin) {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
'X-Pin': verifiedPin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return fetch(url, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the app
|
||||||
|
const initializeApp = () => {
|
||||||
|
// Replace all fetch calls with fetchWithPin
|
||||||
|
window.originalFetch = window.fetch;
|
||||||
|
window.fetch = fetchWithPin;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the app by checking if PIN is required
|
||||||
|
checkPinRequired();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
--text-color: #333;
|
--text-color: #333;
|
||||||
--container-bg: white;
|
--container-bg: white;
|
||||||
--border-color: #ccc;
|
--border-color: #ccc;
|
||||||
--highlight-color: #4CAF50;
|
--highlight-color: #2196F3;
|
||||||
--highlight-hover: #45a049;
|
--highlight-hover: #1976D2;
|
||||||
--progress-bg: #f0f0f0;
|
--progress-bg: #f0f0f0;
|
||||||
|
--primary-color: #2196F3;
|
||||||
|
--secondary-color: #ccc;
|
||||||
|
--textarea-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
--danger-color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
@@ -13,9 +17,13 @@
|
|||||||
--text-color: #fff;
|
--text-color: #fff;
|
||||||
--container-bg: #2d2d2d;
|
--container-bg: #2d2d2d;
|
||||||
--border-color: #404040;
|
--border-color: #404040;
|
||||||
--highlight-color: #4CAF50;
|
--highlight-color: #2196F3;
|
||||||
--highlight-hover: #45a049;
|
--highlight-hover: #1976D2;
|
||||||
--progress-bg: #404040;
|
--progress-bg: #404040;
|
||||||
|
--primary-color: #2196F3;
|
||||||
|
--secondary-color: #404040;
|
||||||
|
--textarea-bg: rgba(255, 255, 255, 0.05);
|
||||||
|
--danger-color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -186,6 +194,115 @@ button:disabled {
|
|||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.visible {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pin-modal .modal-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-header h1 {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-header h2 {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-digit {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid var(--secondary-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--textarea-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1.75rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
caret-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-digit:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-digit.filled {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pin-modal .modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pin-modal .modal-buttons button.primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 120px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pin-modal .modal-buttons button.primary:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pin-modal .modal-buttons button.primary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -198,4 +315,14 @@ button:disabled {
|
|||||||
.upload-container {
|
.upload-container {
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pin-container {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-digit {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
server.js
42
server.js
@@ -9,6 +9,7 @@ const app = express();
|
|||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
const uploadDir = './uploads'; // Local development
|
const uploadDir = './uploads'; // Local development
|
||||||
const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '1024') * 1024 * 1024; // Convert MB to bytes
|
const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '1024') * 1024 * 1024; // Convert MB to bytes
|
||||||
|
const PIN = process.env.DUMBDROP_PIN;
|
||||||
|
|
||||||
// Logging helper
|
// Logging helper
|
||||||
const log = {
|
const log = {
|
||||||
@@ -37,6 +38,9 @@ try {
|
|||||||
fs.accessSync(uploadDir, fs.constants.W_OK);
|
fs.accessSync(uploadDir, fs.constants.W_OK);
|
||||||
log.success(`Upload directory is writable: ${uploadDir}`);
|
log.success(`Upload directory is writable: ${uploadDir}`);
|
||||||
log.info(`Maximum file size set to: ${maxFileSize / (1024 * 1024)}MB`);
|
log.info(`Maximum file size set to: ${maxFileSize / (1024 * 1024)}MB`);
|
||||||
|
if (PIN) {
|
||||||
|
log.info('PIN protection enabled');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`Directory error: ${err.message}`);
|
log.error(`Directory error: ${err.message}`);
|
||||||
log.error(`Failed to access or create upload directory: ${uploadDir}`);
|
log.error(`Failed to access or create upload directory: ${uploadDir}`);
|
||||||
@@ -49,6 +53,44 @@ app.use(cors());
|
|||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Pin verification endpoint
|
||||||
|
app.post('/api/verify-pin', (req, res) => {
|
||||||
|
const { pin } = req.body;
|
||||||
|
|
||||||
|
// If no PIN is set in env, always return success
|
||||||
|
if (!PIN) {
|
||||||
|
return res.json({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the PIN
|
||||||
|
if (pin === PIN) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ success: false, error: 'Invalid PIN' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if PIN is required
|
||||||
|
app.get('/api/pin-required', (req, res) => {
|
||||||
|
res.json({ required: !!PIN });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pin protection middleware
|
||||||
|
const requirePin = (req, res, next) => {
|
||||||
|
if (!PIN) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const providedPin = req.headers['x-pin'];
|
||||||
|
if (providedPin !== PIN) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply pin protection to all /upload routes
|
||||||
|
app.use('/upload', requirePin);
|
||||||
|
|
||||||
// Store ongoing uploads
|
// Store ongoing uploads
|
||||||
const uploads = new Map();
|
const uploads = new Map();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user