-
-
Submit Recording
+
+
Not signed in
+
+ Sign In / Register
+ Logout
-
-
+
-
- Recent Archives
-
-
-
-
+ Recent Archives
+
-
- Load More Calls
-
+ Load More
+
+
diff --git a/cdn.html b/cdn.html
index 4e39fb4..e1c4cbe 100644
--- a/cdn.html
+++ b/cdn.html
@@ -227,7 +227,14 @@ Content-Length: [file-size]
[file-binary-data]
-
+
+
Example
+
danny@daniels-macbook-neo ~ % curl -X POST https://cdn.danielmorrisey.com/ \
+-H "Host: cdn.danielmorrisey.com" \
+-H "Content-Type: image/png" \
+--data-binary "@/Users/danny/Documents/Screenshot 2026-07-30 at 10.05.06.png"
+
+
Response (200 OK)
{
@@ -239,7 +246,17 @@ Content-Length: [file-size]
"size": 2048576
}
+
+
Example
+
{
+ "success":true,
+ "url":"https://cdn.danielmorrisey.com/user-content/2026-07-30/1cb23b40-7621-4b7e-b9a4-61e8d55d35a1.png",
+ "path":"user-content/2026-07-30/1cb23b40-7621-4b7e-b9a4-61e8d55d35a1.png",
+ "contentType":"image/png","fileType":"image","size":7619658
+}
+
+
Rate Limiting (Response Headers)
X-RateLimit-Limit-Files: 30
@@ -250,7 +267,7 @@ X-RateLimit-Reset: 1719878400
-
Limits: 30 files/day, 100 MB/day, 100 MB/file per IP. Resets at midnight UTC.
+
Limits: 70 files/day, 300 MB/day, 100 MB/file per IP. Resets at midnight UTC.
diff --git a/cdn/api.html b/cdn/api.html
new file mode 100644
index 0000000..a9b6be9
--- /dev/null
+++ b/cdn/api.html
@@ -0,0 +1,328 @@
+
+
+
+
+
+ MBD CDN API Docs - danielmorrisey.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ danielmorrisey.com/cdn/api
+ MBD CDN API
+
+ A simple, fast, free CDN powered by Cloudflare R2, D1, and Workers. Upload alsomost anything in seconds, serve it forever. Free for life.
+
+ Expercing Issues? Check our Status Page .
+
+
+
+
+
+ what's been uploaded
+
+
+
+
+
+
+
+
+ Upload
+
+
+ About
+
+
+ How it Works
+
+
+ Build
+
+
+ Limits
+
+
+
+
+
+
+
+ Click to select or drag a file here
+
+
+
+
+
+ ·
+ ·
+
+
+
+
+
+
+
+ Upload to MBD CDN
+
+
+
+
+
+
✓ Your file is live
+
+
+
+
+
+
+
+
+
The MBD CDN is a CDN built on Cloudflare's object storage, R2, a SQLight database, D1, and Cloudflare Workers. With zero cold starts and 100% uptime the MBD stays online to serve millions of people worldwide.
+
+ Upload almost any file (not just images and videos) in seconds (even HTML files) and get a public url anyone can use.
+
+
+
+
+
+
+
+
+
1
+
+
You select a file
+
Your file is read locally in the browser and sent directly to the CDN API over HTTPS — straight to the Cloudflare edge, no intermediate servers.
+
+
+
+
2
+
+
A Worker receives it
+
A Cloudflare Worker handles the upload at the edge. It assigns a UUID filename, detects the file type, and streams the body directly into R2 with no cold starts.
+
+
+
+
3
+
+
R2 stores it permanently
+
The file is written to Cloudflare R2 — S3-compatible storage with zero egress fees and 11 nines of durability. Metadata is logged to D1 to track stats.
+
+
+
+
4
+
+
You get a public URL
+
A permanent cdn.danielmorrisey.com link is returned instantly. Anyone with it can access the file, served from whichever Cloudflare PoP is closest to them.
+
+
+
+
+
+
+
+
310+ Edge Locations
+
Files cached and served globally — sub-50ms for most users.
+
+
+
+
R2 Object Storage
+
Zero egress fees, no expiry. Enterprise-grade durability.
+
+
+
+
Zero Cold Starts
+
Workers run at the edge — every request is handled immediately.
+
+
+
+
+
+
+
+
Build it into your app
+
The MBD CDN API is simple. Send a POST request with your file and get back a JSON response with your permanent URL, size, and file type.
+
+
+
+
POST Request
+
POST https://cdn.danielmorrisey.com/ HTTP/1.1
+Host: cdn.danielmorrisey.com
+Content-Type: [file-mime-type]
+Content-Length: [file-size]
+
+[file-binary-data]
+
+
+
+
Response (200 OK)
+
{
+ "success": true,
+ "url": "https://cdn.danielmorrisey.com/user-content/2026-06-01/uuid.ext",
+ "path": "user-content/2026-06-01/uuid.ext",
+ "contentType": "image/jpeg",
+ "fileType": "image",
+ "size": 2048576
+}
+
+
+
+
Rate Limiting (Response Headers)
+
X-RateLimit-Limit-Files: 30
+X-RateLimit-Remaining-Files: 25
+X-RateLimit-Limit-Bytes: 104857600
+X-RateLimit-Remaining-Bytes: 98765432
+X-RateLimit-Reset: 1719878400
+
+
+
+
Limits: 30 files/day, 100 MB/day, 100 MB/file per IP. Resets at midnight UTC.
+
+
+
+
+
+
+
+
+
—
+
Max File Size
+
Per individual upload
+
+
+
+
—
+
Daily Storage
+
Total uploads per day
+
+
+
+
—
+
Uploads Per Day
+
Resets at midnight UTC
+
+
+
+
+ All limits reset daily at
midnight UTC and are enforced per IP to protect performance for all users.
+
Need higher limits? Try the developer API.
+
+
+
+
your usage today
+
Loading your usage…
+
+
+
+ Files Uploaded
+ 0 / 25
+
+
+
+
+
+ Storage Used
+ 0 B / 200 MB
+
+
+
+
+
+
+
+
+
accepted file types
+
+ Images — JPEG, PNG, WebP, AVIF, SVG ·
+ Animated — GIF ·
+ Video — MP4, WebM, MOV ·
+ Documents — PDF, JSON, HTML, CSS, JS, CSV, Markdown
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/css/alx911.css b/css/alx911.css
index 0f1807e..d06b208 100644
--- a/css/alx911.css
+++ b/css/alx911.css
@@ -261,4 +261,121 @@
flex-direction: column;
gap: 0.25rem;
}
+}
+/* User Status Bar */
+.user-auth-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ padding: 0.75rem 1rem;
+ border-radius: var(--radius);
+ margin-bottom: 1.5rem;
+ font-size: 0.875rem;
+}
+
+.auth-btn {
+ background: var(--bg-raised);
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 0.4rem 0.8rem;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.auth-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+/* Call Card Delete Button */
+.call-author-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.75rem;
+ color: var(--text-dim);
+ margin-bottom: 0.25rem;
+}
+
+.delete-btn {
+ background: rgba(224, 86, 86, 0.15);
+ color: #f88;
+ border: 1px solid #e05656;
+ border-radius: 4px;
+ padding: 0.2rem 0.5rem;
+ cursor: pointer;
+ font-size: 0.75rem;
+}
+
+.delete-btn:hover {
+ background: #e05656;
+ color: #fff;
+}
+
+/* Modal Styling */
+.modal {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 1.5rem;
+ width: 100%;
+ max-width: 360px;
+ position: relative;
+}
+
+.modal-close {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ background: none;
+ border: none;
+ color: var(--text-dim);
+ font-size: 1.25rem;
+ cursor: pointer;
+}
+
+.modal-tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid var(--border);
+ padding-bottom: 0.5rem;
+}
+
+.tab-btn {
+ background: none;
+ border: none;
+ color: var(--text-dim);
+ cursor: pointer;
+ font-weight: 600;
+}
+
+.tab-btn.active {
+ color: var(--accent);
+}
+
+.auth-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.auth-form input {
+ background: var(--bg-raised);
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 0.6rem;
+ border-radius: 6px;
}
\ No newline at end of file
diff --git a/js/alx911.js b/js/alx911.js
index 994a360..67712df 100644
--- a/js/alx911.js
+++ b/js/alx911.js
@@ -1,7 +1,5 @@
(() => {
- // --- API Configuration ---
const API_BASE_URL = 'https://alx911-api.danielmorrisey.com';
-
let nextCursor = null;
// Elements
@@ -12,66 +10,171 @@
const loadMoreContainer = document.getElementById('load-more-container');
const loadMoreBtn = document.getElementById('load-more-btn');
- // UI Utilities
+ // Auth Elements
+ const authModal = document.getElementById('auth-modal');
+ const openAuthBtn = document.getElementById('open-auth-btn');
+ const closeAuthBtn = document.getElementById('close-auth-modal');
+ const logoutBtn = document.getElementById('logout-btn');
+ const userDisplay = document.getElementById('user-display');
+ const authAlert = document.getElementById('auth-alert');
+
+ const tabLogin = document.getElementById('tab-login');
+ const tabRegister = document.getElementById('tab-register');
+ const tabReset = document.getElementById('tab-reset');
+
+ const loginForm = document.getElementById('login-form');
+ const registerForm = document.getElementById('register-form');
+ const forgotForm = document.getElementById('forgot-form');
+
+ // State
+ const getStoredAuth = () => {
+ try {
+ return JSON.parse(localStorage.getItem('alx_auth')) || null;
+ } catch {
+ return null;
+ }
+ };
+
+ const setStoredAuth = (auth) => {
+ if (auth) {
+ localStorage.setItem('alx_auth', JSON.stringify(auth));
+ } else {
+ localStorage.removeItem('alx_auth');
+ }
+ updateAuthUI();
+ };
+
+ const updateAuthUI = () => {
+ const auth = getStoredAuth();
+ if (auth && auth.user) {
+ userDisplay.textContent = `Signed in as @${auth.user.username}`;
+ openAuthBtn.classList.add('hidden');
+ logoutBtn.classList.remove('hidden');
+ } else {
+ userDisplay.textContent = 'Not signed in';
+ openAuthBtn.classList.remove('hidden');
+ logoutBtn.classList.add('hidden');
+ }
+ };
+
+ // UI Helpers
const showAlert = (message, type = 'error') => {
alertBox.className = `alert alert-${type}`;
alertBox.textContent = message;
alertBox.classList.remove('hidden');
};
- const hideAlert = () => {
- alertBox.classList.add('hidden');
+ const showAuthAlert = (message, type = 'error') => {
+ authAlert.className = `alert alert-${type}`;
+ authAlert.textContent = message;
+ authAlert.classList.remove('hidden');
};
- // File Input Visual Feedback
- const setupDropzone = (dropzoneId, inputId) => {
- const dropzone = document.getElementById(dropzoneId);
- const input = document.getElementById(inputId);
- if (!dropzone || !input) return;
-
- const label = dropzone.querySelector('.file-label');
-
- input.addEventListener('change', () => {
- if (input.files.length > 0) {
- label.textContent = input.files[0].name;
- } else {
- label.textContent = inputId === 'audio'
- ? 'Choose or drag audio file'
- : 'Choose or drag video file';
- }
- });
+ // Modal Tab Handler
+ const setTab = (tab) => {
+ authAlert.classList.add('hidden');
+ tabLogin.classList.toggle('active', tab === 'login');
+ tabRegister.classList.toggle('active', tab === 'register');
+ tabReset.classList.toggle('active', tab === 'reset');
- ['dragenter', 'dragover'].forEach(eventName => {
- dropzone.addEventListener(eventName, (e) => {
- e.preventDefault();
- dropzone.classList.add('dragover');
- }, false);
- });
+ loginForm.classList.toggle('hidden', tab !== 'login');
+ registerForm.classList.toggle('hidden', tab !== 'register');
+ forgotForm.classList.toggle('hidden', tab !== 'reset');
+ };
- ['dragleave', 'drop'].forEach(eventName => {
- dropzone.addEventListener(eventName, (e) => {
- e.preventDefault();
- dropzone.classList.remove('dragover');
- }, false);
- });
+ if (tabLogin) tabLogin.onclick = () => setTab('login');
+ if (tabRegister) tabRegister.onclick = () => setTab('register');
+ if (tabReset) tabReset.onclick = () => setTab('reset');
+
+ if (openAuthBtn) openAuthBtn.onclick = () => { authModal.classList.remove('hidden'); setTab('login'); };
+ if (closeAuthBtn) closeAuthBtn.onclick = () => authModal.classList.add('hidden');
+ if (logoutBtn) logoutBtn.onclick = () => { setStoredAuth(null); fetchCalls(); };
+
+ // Login Handler
+ loginForm.onsubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ emailOrUsername: document.getElementById('login-user').value,
+ password: document.getElementById('login-pass').value
+ })
+ });
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || 'Login failed');
+
+ setStoredAuth(data);
+ authModal.classList.add('hidden');
+ fetchCalls();
+ } catch (err) {
+ showAuthAlert(err.message);
+ }
};
- setupDropzone('audio-dropzone', 'audio');
- setupDropzone('video-dropzone', 'video');
+ // Register Handler
+ registerForm.onsubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ username: document.getElementById('reg-user').value,
+ email: document.getElementById('reg-email').value,
+ password: document.getElementById('reg-pass').value
+ })
+ });
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || 'Registration failed');
- const escapeHtml = (str) => {
- return str.replace(/[&<>"']/g, (m) => {
- return {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- }[m];
- });
+ setStoredAuth(data);
+ authModal.classList.add('hidden');
+ fetchCalls();
+ } catch (err) {
+ showAuthAlert(err.message);
+ }
};
- // Render HTML for a Single Call Card
+ // Forgot Password Handler
+ forgotForm.onsubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/auth/request-reset`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email: document.getElementById('forgot-email').value })
+ });
+ const data = await res.json();
+ showAuthAlert(data.message, 'success');
+ } catch (err) {
+ showAuthAlert(err.message);
+ }
+ };
+
+ // Delete Call Handler
+ const deleteCall = async (callId) => {
+ if (!confirm('Are you sure you want to delete this record?')) return;
+
+ const auth = getStoredAuth();
+ if (!auth) return alert('Please sign in first.');
+
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/calls/${callId}`, {
+ method: 'DELETE',
+ headers: { 'Authorization': `Bearer ${auth.token}` }
+ });
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || 'Failed to delete call');
+
+ fetchCalls();
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
+ // Card Renderer
const createCallCard = (call) => {
const card = document.createElement('div');
card.className = 'call-card';
@@ -84,48 +187,56 @@
const audioUrl = `${API_BASE_URL}${call.audioUrl}`;
const videoUrl = call.videoUrl ? `${API_BASE_URL}${call.videoUrl}` : null;
- let videoHtml = '';
- if (videoUrl) {
- videoHtml = `
-
-
- Your browser does not support video playback.
-
- `;
+ let deleteBtnHtml = '';
+ if (call.isOwner) {
+ deleteBtnHtml = ` Delete `;
}
card.innerHTML = `
+
+ Posted by @${call.author}
+ ${deleteBtnHtml}
+
${call.description ? `${escapeHtml(call.description)}
` : ''}
`;
+ const deleteBtn = card.querySelector('.delete-btn');
+ if (deleteBtn) {
+ deleteBtn.onclick = () => deleteCall(call.id);
+ }
+
return card;
};
+ const escapeHtml = (str) => {
+ return str.replace(/[&<>"']/g, (m) => ({
+ '&': '&', '<': '<', '>': '>', '"': '"', "'": '''
+ }[m]));
+ };
+
// API: Fetch Calls List
const fetchCalls = async (cursor = null) => {
try {
const url = new URL(`${API_BASE_URL}/api/calls`);
if (cursor) url.searchParams.set('cursor', cursor);
- const res = await fetch(url.toString());
+ const headers = {};
+ const auth = getStoredAuth();
+ if (auth) headers['Authorization'] = `Bearer ${auth.token}`;
+
+ const res = await fetch(url.toString(), { headers });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
-
- if (!cursor) {
- callsContainer.innerHTML = '';
- }
+ if (!cursor) callsContainer.innerHTML = '';
if (data.items.length === 0 && !cursor) {
callsContainer.innerHTML = 'No call records found.
';
@@ -133,53 +244,42 @@
return;
}
- data.items.forEach(item => {
- callsContainer.appendChild(createCallCard(item));
- });
+ data.items.forEach(item => callsContainer.appendChild(createCallCard(item)));
nextCursor = data.nextCursor;
- if (nextCursor) {
- loadMoreContainer.classList.remove('hidden');
- } else {
- loadMoreContainer.classList.add('hidden');
- }
+ loadMoreContainer.classList.toggle('hidden', !nextCursor);
} catch (err) {
console.error('Failed to load calls:', err);
- if (!cursor) {
- callsContainer.innerHTML = 'Failed to load recordings.
';
- }
}
};
- // API: Form Submit Handler
+ // Upload Form Submit
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
- hideAlert();
+ const auth = getStoredAuth();
+ if (!auth) {
+ showAlert('You must be signed in to submit recordings.');
+ openAuthBtn.click();
+ return;
+ }
const formData = new FormData(form);
-
submitBtn.disabled = true;
submitBtn.innerHTML = ' Uploading...';
try {
const res = await fetch(`${API_BASE_URL}/api/calls`, {
method: 'POST',
+ headers: { 'Authorization': `Bearer ${auth.token}` },
body: formData
});
const result = await res.json();
-
- if (!res.ok) {
- throw new Error(result.error || 'Failed to submit recording.');
- }
+ if (!res.ok) throw new Error(result.error || 'Failed to submit recording.');
showAlert('Record successfully submitted!', 'success');
form.reset();
- document.querySelector('#audio-dropzone .file-label').textContent = 'Choose or drag audio file';
- document.querySelector('#video-dropzone .file-label').textContent = 'Choose or drag video file';
-
- // Refresh call feed instantly
fetchCalls();
} catch (err) {
showAlert(err.message || 'An error occurred while uploading.');
@@ -190,15 +290,11 @@
});
}
- // Pagination Load More
if (loadMoreBtn) {
- loadMoreBtn.addEventListener('click', () => {
- if (nextCursor) {
- fetchCalls(nextCursor);
- }
- });
+ loadMoreBtn.onclick = () => { if (nextCursor) fetchCalls(nextCursor); };
}
- // Initial Load
+ // Init
+ updateAuthUI();
fetchCalls();
})();
\ No newline at end of file