security: replace live tunnel URL with placeholder API key and URL in public repo
This commit is contained in:
parent
7f3c84763e
commit
f13eaa279f
4 changed files with 75 additions and 15 deletions
33
README.md
33
README.md
|
|
@ -1,16 +1,42 @@
|
||||||
# katdrop
|
# 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
|
## features
|
||||||
|
|
||||||
- minimal dark interface with smooth interactive particle background
|
- 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
|
- drag & drop files anywhere to upload instantly
|
||||||
- 10-minute auto-deletion timer per file with live countdown
|
- 10-minute auto-deletion timer per file with live countdown
|
||||||
- sleek upload progress bar
|
|
||||||
- safe path verification and collision-free file naming
|
- safe path verification and collision-free file naming
|
||||||
|
- optional API key authorization (`x-katdrop-key`)
|
||||||
- zero external CDN dependencies (works completely offline)
|
- 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
|
## usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -25,8 +51,9 @@ Runs by default on `http://localhost:3000` (or falls back to an available port i
|
||||||
|
|
||||||
## endpoints
|
## endpoints
|
||||||
|
|
||||||
|
- `GET /health` - health check & uptime
|
||||||
- `POST /upload` - upload files (`file`)
|
- `POST /upload` - upload files (`file`)
|
||||||
- `GET /files` - list files
|
- `GET /files` - list active files with remaining TTL
|
||||||
- `GET /uploads/:filename` - download file
|
- `GET /uploads/:filename` - download file
|
||||||
- `DELETE /delete/:filename` - delete file
|
- `DELETE /delete/:filename` - delete file
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
/files https://feof-signals-fix-chevy.trycloudflare.com/files 200
|
# Netlify API Proxying
|
||||||
/upload https://feof-signals-fix-chevy.trycloudflare.com/upload 200
|
# Replace YOUR_BACKEND_URL with your live Cloudflare Tunnel URL:
|
||||||
/uploads/* https://feof-signals-fix-chevy.trycloudflare.com/uploads/:splat 200
|
# /files https://YOUR_BACKEND_URL.trycloudflare.com/files 200
|
||||||
/delete/* https://feof-signals-fix-chevy.trycloudflare.com/delete/:splat 200
|
# /upload https://YOUR_BACKEND_URL.trycloudflare.com/upload 200
|
||||||
/* /index.html 200
|
# /uploads/* https://YOUR_BACKEND_URL.trycloudflare.com/uploads/:splat 200
|
||||||
|
# /delete/* https://YOUR_BACKEND_URL.trycloudflare.com/delete/:splat 200
|
||||||
|
/* /index.html 200
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,29 @@
|
||||||
|
|
||||||
let selectedFiles = [];
|
let selectedFiles = [];
|
||||||
|
|
||||||
// API Base configuration for remote frontend (Netlify) or local hosting
|
// API & Key Configuration (configurable via URL param ?api=...&key=... or localStorage)
|
||||||
const DEFAULT_REMOTE_API = 'https://feof-signals-fix-chevy.trycloudflare.com';
|
// Public repo uses placeholder values for security
|
||||||
|
const DEFAULT_REMOTE_API = 'https://YOUR_BACKEND_URL.trycloudflare.com';
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.has('api')) {
|
if (urlParams.has('api')) {
|
||||||
const customApi = urlParams.get('api').replace(/\/+$/, '');
|
const customApi = urlParams.get('api').replace(/\/+$/, '');
|
||||||
localStorage.setItem('katdrop_api', customApi);
|
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 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_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
|
// Toast
|
||||||
let toastTimeout;
|
let toastTimeout;
|
||||||
|
|
@ -59,7 +73,7 @@
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
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);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -267,7 +281,7 @@
|
||||||
// Fetch and display files
|
// Fetch and display files
|
||||||
async function fetchFiles() {
|
async function fetchFiles() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/files`);
|
const res = await apiFetch(`${API_BASE}/files`);
|
||||||
const files = await res.json();
|
const files = await res.json();
|
||||||
fileList.innerHTML = '';
|
fileList.innerHTML = '';
|
||||||
|
|
||||||
|
|
@ -288,6 +302,7 @@
|
||||||
const size = typeof file === 'object' && file.sizeFormatted ? file.sizeFormatted : '';
|
const size = typeof file === 'object' && file.sizeFormatted ? file.sizeFormatted : '';
|
||||||
const expiresMs = file.expiresAt ? new Date(file.expiresAt).getTime() : (Date.now() + 600000);
|
const expiresMs = file.expiresAt ? new Date(file.expiresAt).getTime() : (Date.now() + 600000);
|
||||||
const remainingSec = Math.max(0, Math.ceil((expiresMs - Date.now()) / 1000));
|
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 = `
|
li.innerHTML = `
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
|
|
@ -298,7 +313,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-actions">
|
<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>
|
<button class="action-btn action-delete" onclick="deleteFile('${escapeHtml(name)}', this.closest('li'))">delete</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -317,7 +332,7 @@
|
||||||
if (cardElement) cardElement.classList.add('deleting');
|
if (cardElement) cardElement.classList.add('deleting');
|
||||||
|
|
||||||
try {
|
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) {
|
if (res.ok) {
|
||||||
showToast('file deleted');
|
showToast('file deleted');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -397,6 +412,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.open('POST', `${API_BASE}/upload`);
|
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);
|
xhr.send(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
15
server.js
15
server.js
|
|
@ -95,7 +95,7 @@ const upload = multer({
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
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') {
|
if (req.method === 'OPTIONS') {
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +104,19 @@ app.use((req, res, next) => {
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(publicDir));
|
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
|
// Health check endpoint
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({ status: 'ok', uptime: Math.floor(process.uptime()) });
|
res.json({ status: 'ok', uptime: Math.floor(process.uptime()) });
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue