|
| 1 | +const API_BASE = "https://cipherstream-backend.vercel.app/api"; |
| 2 | + |
| 3 | +let token = localStorage.getItem("cs_admin_token") || null; |
| 4 | + |
| 5 | +const loginCard = document.getElementById("loginCard"); |
| 6 | +const panel = document.getElementById("panel"); |
| 7 | +const loginMsg = document.getElementById("loginMsg"); |
| 8 | +const logoutBtn = document.getElementById("logoutBtn"); |
| 9 | +const channelTable = document.getElementById("channelTable"); |
| 10 | +const channelCount = document.getElementById("channelCount"); |
| 11 | +const addForm = document.getElementById("addForm"); |
| 12 | +const toggleAddForm = document.getElementById("toggleAddForm"); |
| 13 | +const cancelAdd = document.getElementById("cancelAdd"); |
| 14 | +const addMsg = document.getElementById("addMsg"); |
| 15 | + |
| 16 | +function setSignedIn(isSignedIn) { |
| 17 | + loginCard.classList.toggle("hidden", isSignedIn); |
| 18 | + panel.classList.toggle("hidden", !isSignedIn); |
| 19 | + logoutBtn.classList.toggle("hidden", !isSignedIn); |
| 20 | +} |
| 21 | + |
| 22 | +function logout() { |
| 23 | + token = null; |
| 24 | + localStorage.removeItem("cs_admin_token"); |
| 25 | + setSignedIn(false); |
| 26 | +} |
| 27 | +logoutBtn.addEventListener("click", logout); |
| 28 | + |
| 29 | +async function api(path, opts = {}) { |
| 30 | + const headers = { "Content-Type": "application/json", ...(opts.headers || {}) }; |
| 31 | + if (token) headers.Authorization = `Bearer ${token}`; |
| 32 | + const res = await fetch(`${API_BASE}${path}`, { ...opts, headers }); |
| 33 | + const data = await res.json().catch(() => ({})); |
| 34 | + if (res.status === 401) { |
| 35 | + logout(); |
| 36 | + throw new Error("Session expired — please log in again."); |
| 37 | + } |
| 38 | + if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`); |
| 39 | + return data; |
| 40 | +} |
| 41 | + |
| 42 | +document.getElementById("loginForm").addEventListener("submit", async (e) => { |
| 43 | + e.preventDefault(); |
| 44 | + loginMsg.textContent = "logging in..."; |
| 45 | + try { |
| 46 | + const email = document.getElementById("admin-email").value; |
| 47 | + const password = document.getElementById("admin-password").value; |
| 48 | + const data = await api("/admin/login", { method: "POST", body: JSON.stringify({ email, password }) }); |
| 49 | + token = data.token; |
| 50 | + localStorage.setItem("cs_admin_token", token); |
| 51 | + setSignedIn(true); |
| 52 | + loadChannels(); |
| 53 | + } catch (err) { |
| 54 | + loginMsg.textContent = err.message; |
| 55 | + } |
| 56 | +}); |
| 57 | + |
| 58 | +async function loadChannels() { |
| 59 | + channelTable.innerHTML = `<tr><td colspan="7">loading...</td></tr>`; |
| 60 | + try { |
| 61 | + const data = await api("/admin/channels"); |
| 62 | + channelCount.textContent = data.channels.length; |
| 63 | + renderTable(data.channels); |
| 64 | + } catch (err) { |
| 65 | + channelTable.innerHTML = `<tr><td colspan="7">${err.message}</td></tr>`; |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +function renderTable(channels) { |
| 70 | + channelTable.innerHTML = ""; |
| 71 | + channels.forEach((ch) => { |
| 72 | + const tr = document.createElement("tr"); |
| 73 | + if (!ch.isActive) tr.className = "inactive"; |
| 74 | + tr.innerHTML = ` |
| 75 | + <td>${ch.channelNumber ?? "—"}</td> |
| 76 | + <td>${ch.displayName}</td> |
| 77 | + <td><span class="badge ${ch.contentType}">${ch.contentType}</span></td> |
| 78 | + <td>${ch.category}</td> |
| 79 | + <td>${ch.tier}</td> |
| 80 | + <td class="url-cell" title="${ch.streamUrl}">${ch.streamUrl}</td> |
| 81 | + <td class="row-actions"> |
| 82 | + <button class="ghost edit-btn">Edit URL</button> |
| 83 | + ${ch.isActive |
| 84 | + ? `<button class="danger deactivate-btn">Deactivate</button>` |
| 85 | + : `<button class="reactivate-btn">Reactivate</button>`} |
| 86 | + <button class="danger delete-btn">Delete</button> |
| 87 | + </td> |
| 88 | + `; |
| 89 | + tr.querySelector(".edit-btn").addEventListener("click", () => editStreamUrl(ch)); |
| 90 | + if (ch.isActive) { |
| 91 | + tr.querySelector(".deactivate-btn").addEventListener("click", () => deactivate(ch._id)); |
| 92 | + } else { |
| 93 | + tr.querySelector(".reactivate-btn").addEventListener("click", () => reactivate(ch._id)); |
| 94 | + } |
| 95 | + tr.querySelector(".delete-btn").addEventListener("click", () => deleteChannel(ch._id, ch.displayName)); |
| 96 | + channelTable.appendChild(tr); |
| 97 | + }); |
| 98 | +} |
| 99 | + |
| 100 | +async function editStreamUrl(channel) { |
| 101 | + const newUrl = prompt(`New stream URL for "${channel.displayName}":`, channel.streamUrl); |
| 102 | + if (!newUrl || newUrl === channel.streamUrl) return; |
| 103 | + try { |
| 104 | + await api(`/admin/channels/${channel._id}`, { method: "PATCH", body: JSON.stringify({ streamUrl: newUrl }) }); |
| 105 | + loadChannels(); |
| 106 | + } catch (err) { |
| 107 | + alert(err.message); |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +async function deactivate(id) { |
| 112 | + try { |
| 113 | + await api(`/admin/channels/${id}/deactivate`, { method: "PATCH" }); |
| 114 | + loadChannels(); |
| 115 | + } catch (err) { |
| 116 | + alert(err.message); |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +async function reactivate(id) { |
| 121 | + try { |
| 122 | + await api(`/admin/channels/${id}/reactivate`, { method: "PATCH" }); |
| 123 | + loadChannels(); |
| 124 | + } catch (err) { |
| 125 | + alert(err.message); |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +async function deleteChannel(id, name) { |
| 130 | + if (!confirm(`Permanently delete "${name}"? This can't be undone — consider Deactivate instead.`)) return; |
| 131 | + try { |
| 132 | + await api(`/admin/channels/${id}`, { method: "DELETE" }); |
| 133 | + loadChannels(); |
| 134 | + } catch (err) { |
| 135 | + alert(err.message); |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +toggleAddForm.addEventListener("click", () => addForm.classList.toggle("open")); |
| 140 | +cancelAdd.addEventListener("click", () => addForm.classList.remove("open")); |
| 141 | + |
| 142 | +document.getElementById("submitAdd").addEventListener("click", async () => { |
| 143 | + const contentType = document.getElementById("new-contentType").value; |
| 144 | + const channelNumber = document.getElementById("new-channelNumber").value; |
| 145 | + const payload = { |
| 146 | + displayName: document.getElementById("new-displayName").value, |
| 147 | + contentType, |
| 148 | + category: document.getElementById("new-category").value, |
| 149 | + bundleId: document.getElementById("new-bundleId").value, |
| 150 | + tier: document.getElementById("new-tier").value, |
| 151 | + streamUrl: document.getElementById("new-streamUrl").value, |
| 152 | + description: document.getElementById("new-description").value, |
| 153 | + }; |
| 154 | + if (contentType === "live" && channelNumber) payload.channelNumber = Number(channelNumber); |
| 155 | + |
| 156 | + if (!payload.displayName || !payload.bundleId || !payload.streamUrl) { |
| 157 | + addMsg.textContent = "Name, bundle ID, and stream URL are required."; |
| 158 | + return; |
| 159 | + } |
| 160 | + |
| 161 | + addMsg.textContent = "saving..."; |
| 162 | + try { |
| 163 | + await api("/admin/channels", { method: "POST", body: JSON.stringify(payload) }); |
| 164 | + addMsg.textContent = ""; |
| 165 | + addForm.classList.remove("open"); |
| 166 | + ["new-displayName", "new-channelNumber", "new-bundleId", "new-streamUrl", "new-description"].forEach( |
| 167 | + (id) => (document.getElementById(id).value = "") |
| 168 | + ); |
| 169 | + loadChannels(); |
| 170 | + } catch (err) { |
| 171 | + addMsg.textContent = err.message; |
| 172 | + } |
| 173 | +}); |
| 174 | + |
| 175 | +setSignedIn(!!token); |
| 176 | +if (token) loadChannels(); |
0 commit comments