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
- Configurable file size limits
- Drag and Drop Directory Support (Maintains file structure in upload)
- Optional PIN protection
# Future Features
- Camera Upload for Mobile
@@ -35,7 +36,8 @@ npm install
2. Set environment variables in `.env`:
```env
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:
@@ -52,10 +54,10 @@ docker pull abite3/dumbdrop:latest
# Run the container
# 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:
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
@@ -67,19 +69,20 @@ docker build -t dumbdrop .
2. Run the container:
```bash
# 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:
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
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"
3. Select one or multiple files
4. Click "Upload Files"
5. Files will be saved to:
2. If PIN protection is enabled, enter the 4-digit PIN
3. Drag and drop files into the upload area or click "Browse Files"
4. Select one or multiple files
5. Click "Upload Files"
6. Files will be saved to:
- Local development: `./uploads` directory
- 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
- Frontend: Vanilla JavaScript with modern drag-and-drop API
- File handling: Chunked file uploads with configurable size limits
- Security: Optional PIN protection for uploads
- Containerization: Docker with automated builds via GitHub Actions

View File

@@ -3,12 +3,31 @@
<head>
<meta charset="UTF-8">
<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="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
</head>
<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">
<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">
@@ -26,7 +45,7 @@
<line class="sun" x1="18.36" y1="5.64" x2="19.78" y2="4.22" style="display:none"/>
</svg>
</button>
<h1>Dumb Drop</h1>
<h1>DumbDrop</h1>
<div class="upload-container" id="dropZone">
<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">
@@ -412,6 +431,147 @@
const savedTheme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
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>
</body>
</html>

View File

@@ -3,9 +3,13 @@
--text-color: #333;
--container-bg: white;
--border-color: #ccc;
--highlight-color: #4CAF50;
--highlight-hover: #45a049;
--highlight-color: #2196F3;
--highlight-hover: #1976D2;
--progress-bg: #f0f0f0;
--primary-color: #2196F3;
--secondary-color: #ccc;
--textarea-bg: rgba(255, 255, 255, 0.1);
--danger-color: #f44336;
}
[data-theme="dark"] {
@@ -13,9 +17,13 @@
--text-color: #fff;
--container-bg: #2d2d2d;
--border-color: #404040;
--highlight-color: #4CAF50;
--highlight-hover: #45a049;
--highlight-color: #2196F3;
--highlight-hover: #1976D2;
--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;
}
/* 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) {
.container {
padding: 10px;
@@ -198,4 +315,14 @@ button:disabled {
.upload-container {
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 uploadDir = './uploads'; // Local development
const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '1024') * 1024 * 1024; // Convert MB to bytes
const PIN = process.env.DUMBDROP_PIN;
// Logging helper
const log = {
@@ -37,6 +38,9 @@ try {
fs.accessSync(uploadDir, fs.constants.W_OK);
log.success(`Upload directory is writable: ${uploadDir}`);
log.info(`Maximum file size set to: ${maxFileSize / (1024 * 1024)}MB`);
if (PIN) {
log.info('PIN protection enabled');
}
} catch (err) {
log.error(`Directory error: ${err.message}`);
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.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
const uploads = new Map();