security: replace live tunnel URL with placeholder API key and URL in public repo

This commit is contained in:
katR 2026-09-12 10:16:23 +05:30
parent 7f3c84763e
commit f13eaa279f
4 changed files with 75 additions and 15 deletions

View file

@ -1,16 +1,42 @@
# katdrop
a minimal, smooth file transfer service.
a minimal, smooth file transfer service with 10-minute auto-deletion.
> **Note**: This repository uses a placeholder backend URL (`https://YOUR_BACKEND_URL.trycloudflare.com`) and placeholder API key (`YOUR_API_KEY_HERE`) so no private endpoints or keys are exposed publicly.
## features
- minimal dark interface with smooth interactive particle background
- real-time backend status indicator (`online` / `offline`)
- live upload byte progress and percentage
- drag & drop files anywhere to upload instantly
- 10-minute auto-deletion timer per file with live countdown
- sleek upload progress bar
- safe path verification and collision-free file naming
- optional API key authorization (`x-katdrop-key`)
- zero external CDN dependencies (works completely offline)
## configuration
### connecting your frontend (Netlify) to your backend
To connect your deployed frontend to your private backend (e.g. Cloudflare Tunnel or local server), pass your API URL and optional key via URL parameters once:
```
https://your-site.netlify.app/?api=https://your-tunnel.trycloudflare.com&key=YOUR_SECRET_KEY
```
KatDrop will automatically save these to `localStorage` in your browser.
### backend API key (optional)
To require an API key on your backend, set `KATDROP_KEY`:
```bash
KATDROP_KEY=mysecretkey npm start
```
If not set, the backend runs without key verification.
## usage
```bash
@ -25,8 +51,9 @@ Runs by default on `http://localhost:3000` (or falls back to an available port i
## endpoints
- `GET /health` - health check & uptime
- `POST /upload` - upload files (`file`)
- `GET /files` - list files
- `GET /files` - list active files with remaining TTL
- `GET /uploads/:filename` - download file
- `DELETE /delete/:filename` - delete file

View file

@ -1,5 +1,7 @@
/files https://feof-signals-fix-chevy.trycloudflare.com/files 200
/upload https://feof-signals-fix-chevy.trycloudflare.com/upload 200
/uploads/* https://feof-signals-fix-chevy.trycloudflare.com/uploads/:splat 200
/delete/* https://feof-signals-fix-chevy.trycloudflare.com/delete/:splat 200
/* /index.html 200
# Netlify API Proxying
# Replace YOUR_BACKEND_URL with your live Cloudflare Tunnel URL:
# /files https://YOUR_BACKEND_URL.trycloudflare.com/files 200
# /upload https://YOUR_BACKEND_URL.trycloudflare.com/upload 200
# /uploads/* https://YOUR_BACKEND_URL.trycloudflare.com/uploads/:splat 200
# /delete/* https://YOUR_BACKEND_URL.trycloudflare.com/delete/:splat 200
/* /index.html 200

View file

@ -20,15 +20,29 @@
let selectedFiles = [];
// API Base configuration for remote frontend (Netlify) or local hosting
const DEFAULT_REMOTE_API = 'https://feof-signals-fix-chevy.trycloudflare.com';
// API & Key Configuration (configurable via URL param ?api=...&key=... or localStorage)
// Public repo uses placeholder values for security
const DEFAULT_REMOTE_API = 'https://YOUR_BACKEND_URL.trycloudflare.com';
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('api')) {
const customApi = urlParams.get('api').replace(/\/+$/, '');
localStorage.setItem('katdrop_api', customApi);
}
if (urlParams.has('key')) {
localStorage.setItem('katdrop_key', urlParams.get('key'));
}
const isLocalOrigin = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.');
const API_BASE = window.KATDROP_API || localStorage.getItem('katdrop_api') || (isLocalOrigin ? '' : DEFAULT_REMOTE_API);
const API_KEY = window.KATDROP_KEY || localStorage.getItem('katdrop_key') || 'YOUR_API_KEY_HERE';
// Authenticated fetch helper
function apiFetch(url, options = {}) {
options.headers = options.headers || {};
if (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') {
options.headers['x-katdrop-key'] = API_KEY;
}
return fetch(url, options);
}
// Toast
let toastTimeout;
@ -59,7 +73,7 @@
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 4000);
const res = await fetch(`${API_BASE}/health`, { signal: controller.signal });
const res = await apiFetch(`${API_BASE}/health`, { signal: controller.signal });
clearTimeout(timeoutId);
if (res.ok) {
@ -267,7 +281,7 @@
// Fetch and display files
async function fetchFiles() {
try {
const res = await fetch(`${API_BASE}/files`);
const res = await apiFetch(`${API_BASE}/files`);
const files = await res.json();
fileList.innerHTML = '';
@ -288,6 +302,7 @@
const size = typeof file === 'object' && file.sizeFormatted ? file.sizeFormatted : '';
const expiresMs = file.expiresAt ? new Date(file.expiresAt).getTime() : (Date.now() + 600000);
const remainingSec = Math.max(0, Math.ceil((expiresMs - Date.now()) / 1000));
const keyParam = (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') ? `?key=${encodeURIComponent(API_KEY)}` : '';
li.innerHTML = `
<div class="file-info">
@ -298,7 +313,7 @@
</div>
</div>
<div class="file-actions">
<a href="${API_BASE}/uploads/${encodeURIComponent(name)}" class="action-btn action-download" download>download</a>
<a href="${API_BASE}/uploads/${encodeURIComponent(name)}${keyParam}" class="action-btn action-download" download>download</a>
<button class="action-btn action-delete" onclick="deleteFile('${escapeHtml(name)}', this.closest('li'))">delete</button>
</div>
`;
@ -317,7 +332,7 @@
if (cardElement) cardElement.classList.add('deleting');
try {
const res = await fetch(`${API_BASE}/delete/${encodeURIComponent(filename)}`, { method: 'DELETE' });
const res = await apiFetch(`${API_BASE}/delete/${encodeURIComponent(filename)}`, { method: 'DELETE' });
if (res.ok) {
showToast('file deleted');
setTimeout(() => {
@ -397,6 +412,9 @@
};
xhr.open('POST', `${API_BASE}/upload`);
if (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') {
xhr.setRequestHeader('x-katdrop-key', API_KEY);
}
xhr.send(formData);
}

View file

@ -95,7 +95,7 @@ const upload = multer({
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-katdrop-key');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
@ -104,6 +104,19 @@ app.use((req, res, next) => {
app.use(express.json());
app.use(express.static(publicDir));
// Optional API Key Protection (if KATDROP_KEY environment variable is set)
const REQUIRED_API_KEY = process.env.KATDROP_KEY || '';
if (REQUIRED_API_KEY) {
app.use((req, res, next) => {
if (req.method === 'OPTIONS' || req.path === '/health' || req.path === '/') return next();
const clientKey = req.headers['x-katdrop-key'] || req.query.key;
if (clientKey !== REQUIRED_API_KEY) {
return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key' });
}
next();
});
}
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: Math.floor(process.uptime()) });