Add simple auth

Added Pin verification, set as DUMBDROP_PIN env variable.
This commit is contained in:
abiteman
2025-01-24 11:44:15 -06:00
parent e9ad4a8ac5
commit c2ee46717f
4 changed files with 348 additions and 15 deletions

View File

@@ -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
@@ -35,7 +36,8 @@ npm install
2. Set environment variables in `.env`: 2. Set environment variables in `.env`:
```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

View File

@@ -3,12 +3,31 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dumb Drop - Simple File Upload</title> <title>DumbDrop - Simple File Upload</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.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> <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">
@@ -26,7 +45,7 @@
<line class="sun" x1="18.36" y1="5.64" x2="19.78" y2="4.22" style="display:none"/> <line class="sun" x1="18.36" y1="5.64" x2="19.78" y2="4.22" style="display:none"/>
</svg> </svg>
</button> </button>
<h1>Dumb Drop</h1> <h1>DumbDrop</h1>
<div class="upload-container" id="dropZone"> <div class="upload-container" id="dropZone">
<div class="upload-content"> <div class="upload-content">
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" 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" width="50" height="50" 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>

View File

@@ -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;
}
} }

View File

@@ -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();