const token = localStorage.getItem("indiko_session"); const footer = document.getElementById("footer") as HTMLElement; const clientsList = document.getElementById("clientsList") as HTMLElement; const createClientBtn = document.getElementById( "createClientBtn", ) as HTMLButtonElement; const clientModal = document.getElementById("clientModal") as HTMLElement; const modalClose = document.getElementById("modalClose") as HTMLButtonElement; const cancelBtn = document.getElementById("cancelBtn") as HTMLButtonElement; const clientForm = document.getElementById("clientForm") as HTMLFormElement; const modalTitle = document.getElementById("modalTitle") as HTMLElement; const addRedirectUriBtn = document.getElementById( "addRedirectUriBtn", ) as HTMLButtonElement; const redirectUrisList = document.getElementById( "redirectUrisList", ) as HTMLElement; const toast = document.getElementById("toast") as HTMLElement; function showToast(message: string, type: "success" | "error" = "success") { toast.textContent = message; toast.className = `toast ${type} show`; setTimeout(() => { toast.classList.remove("show"); }, 3000); } async function checkAuth() { if (!token) { window.location.href = "/login"; return; } try { const response = await fetch("/api/hello", { headers: { Authorization: `Bearer ${token}`, }, }); if (response.status === 401 || response.status === 403) { localStorage.removeItem("indiko_session"); window.location.href = "/login"; return; } const data = await response.json(); footer.innerHTML = `admin • signed in as ${data.username}sign out `; document .getElementById("logoutLink") ?.addEventListener("click", async (e) => { e.preventDefault(); try { await fetch("/auth/logout", { method: "POST", headers: { Authorization: `Bearer ${token}`, }, }); } catch { // Ignore logout errors } localStorage.removeItem("indiko_session"); window.location.href = "/login"; }); if (!data.isAdmin) { window.location.href = "/"; return; } loadClients(); } catch (error) { console.error("Auth check failed:", error); footer.textContent = "error loading user info"; clientsList.innerHTML = '
Failed to load clients
'; } } interface Client { id: number; clientId: string; name: string; logoUrl: string | null; description: string | null; redirectUris: string[]; isPreregistered: boolean; availableRoles: string[] | null; defaultRole: string | null; firstSeen: number; lastUsed: number; } interface ClientUser { username: string; name: string; scopes: string[]; role: string | null; grantedAt: number; lastUsed: number; } interface AppPermission { username: string; name: string; scopes: string[]; grantedAt: number; lastUsed: number; } async function loadClients() { try { const response = await fetch("/api/admin/clients", { headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error("Failed to load clients"); } const data = await response.json(); displayClients(data.clients); } catch (error) { console.error("Failed to load clients:", error); clientsList.innerHTML = '
Failed to load clients
'; } } function displayClients(clients: Client[]) { if (clients.length === 0) { clientsList.innerHTML = '
No OAuth clients registered yet.
'; return; } clientsList.innerHTML = clients .map((client) => { const lastUsedDate = new Date( client.lastUsed * 1000, ).toLocaleDateString(); const firstSeenDate = new Date( client.firstSeen * 1000, ).toLocaleDateString(); return `
${client.name}
${client.clientId}
${client.description ? `
${client.description}
` : ""}
${client.isPreregistered ? "pre-registered" : "auto-registered"} first seen ${firstSeenDate} last used ${lastUsedDate}
${ client.isPreregistered ? ` ` : "" } details
loading details...
`; }) .join(""); } (window as any).toggleClient = async (clientId: string) => { const card = document.querySelector( `[data-client-id="${clientId}"]`, ) as HTMLElement; if (!card) return; const isExpanded = card.classList.contains("expanded"); const arrow = card.querySelector(".arrow") as HTMLElement; if (isExpanded) { card.classList.remove("expanded"); if (arrow) arrow.textContent = "▼"; return; } card.classList.add("expanded"); if (arrow) arrow.textContent = "▲"; const detailsDiv = document.getElementById( `details-${encodeURIComponent(clientId)}`, ); if (!detailsDiv) return; if (detailsDiv.dataset.loaded === "true") { return; } try { const response = await fetch( `/api/admin/clients/${encodeURIComponent(clientId)}`, { headers: { Authorization: `Bearer ${token}`, }, }, ); if (!response.ok) { throw new Error("Failed to load client details"); } const data = await response.json(); detailsDiv.innerHTML = ` ${ data.client.isPreregistered ? `
client secret
` : "" }
redirect uris
${data.client.redirectUris.map((uri: string) => `
${uri}
`).join("")}
authorized users (${data.users.length})
${ data.users.length === 0 ? '
No users have authorized this client yet
' : `
${data.users .map((user: ClientUser) => { const grantedDate = new Date( user.grantedAt * 1000, ).toLocaleDateString(); const lastUsedDate = new Date( user.lastUsed * 1000, ).toLocaleDateString(); return `
`; }) .join("")}
` }
`; detailsDiv.dataset.loaded = "true"; } catch (error) { console.error("Failed to load client details:", error); detailsDiv.innerHTML = '
Failed to load details
'; } }; (window as any).setUserRole = async ( clientId: string, username: string, role: string, ) => { try { const response = await fetch( `/api/admin/clients/${encodeURIComponent(clientId)}/users/${encodeURIComponent(username)}/role`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ role: role || null }), }, ); if (!response.ok) { throw new Error("Failed to set user role"); } showToast("User role updated successfully"); } catch (error) { console.error("Failed to set user role:", error); showToast("Failed to update user role. Please try again.", "error"); } }; (window as any).editClient = async (clientId: string) => { try { const response = await fetch( `/api/admin/clients/${encodeURIComponent(clientId)}`, { headers: { Authorization: `Bearer ${token}`, }, }, ); if (!response.ok) { throw new Error("Failed to load client"); } const data = await response.json(); const client = data.client; modalTitle.textContent = "Edit OAuth Client"; (document.getElementById("editClientId") as HTMLInputElement).value = clientId; (document.getElementById("clientName") as HTMLInputElement).value = client.name || ""; (document.getElementById("logoUrl") as HTMLInputElement).value = client.logoUrl || ""; (document.getElementById("description") as HTMLTextAreaElement).value = client.description || ""; (document.getElementById("availableRoles") as HTMLTextAreaElement).value = client.availableRoles ? client.availableRoles.join("\n") : ""; (document.getElementById("defaultRole") as HTMLInputElement).value = client.defaultRole || ""; redirectUrisList.innerHTML = client.redirectUris .map( (uri: string) => `
`, ) .join(""); clientModal.classList.add("active"); } catch (error) { console.error("Failed to load client:", error); showToast("Failed to load client details", "error"); } }; (window as any).deleteClient = async (clientId: string, event?: Event) => { const btn = event?.target as HTMLButtonElement | undefined; // Double-click confirmation pattern if (btn?.dataset.confirmState === "pending") { // Second click - execute delete delete btn.dataset.confirmState; btn.disabled = true; btn.textContent = "deleting..."; try { const response = await fetch( `/api/admin/clients/${encodeURIComponent(clientId)}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}`, }, }, ); if (!response.ok) { throw new Error("Failed to delete client"); } await loadClients(); } catch (error) { console.error("Failed to delete client:", error); showToast("Failed to delete client. Please try again.", "error"); btn.disabled = false; btn.textContent = "delete"; } } else { // First click - set pending state if (btn) { const originalText = btn.textContent; btn.dataset.confirmState = "pending"; btn.textContent = "you sure?"; // Reset after 3 seconds if not confirmed setTimeout(() => { if (btn.dataset.confirmState === "pending") { delete btn.dataset.confirmState; btn.textContent = originalText; } }, 3000); } } }; createClientBtn.addEventListener("click", () => { modalTitle.textContent = "Create OAuth Client"; clientForm.reset(); (document.getElementById("editClientId") as HTMLInputElement).value = ""; redirectUrisList.innerHTML = `
`; clientModal.classList.add("active"); }); modalClose.addEventListener("click", () => { clientModal.classList.remove("active"); }); cancelBtn.addEventListener("click", () => { clientModal.classList.remove("active"); }); addRedirectUriBtn.addEventListener("click", () => { const newItem = document.createElement("div"); newItem.className = "redirect-uri-item"; newItem.innerHTML = ` `; redirectUrisList.appendChild(newItem); }); (window as any).removeRedirectUri = (btn: HTMLButtonElement) => { const items = redirectUrisList.querySelectorAll(".redirect-uri-item"); if (items.length > 1) { btn.parentElement?.remove(); } else { showToast("At least one redirect URI is required", "error"); } }; clientForm.addEventListener("submit", async (e) => { e.preventDefault(); const editClientId = ( document.getElementById("editClientId") as HTMLInputElement ).value; const name = (document.getElementById("clientName") as HTMLInputElement) .value; const logoUrl = (document.getElementById("logoUrl") as HTMLInputElement) .value; const description = ( document.getElementById("description") as HTMLTextAreaElement ).value; const availableRolesText = ( document.getElementById("availableRoles") as HTMLTextAreaElement ).value; const defaultRole = ( document.getElementById("defaultRole") as HTMLInputElement ).value; const redirectUriInputs = Array.from( redirectUrisList.querySelectorAll(".redirect-uri-input"), ) as HTMLInputElement[]; const redirectUris = redirectUriInputs .map((input) => input.value) .filter((uri) => uri.trim()); // Parse available roles from textarea (one per line) const availableRoles = availableRolesText .split("\n") .map((r) => r.trim()) .filter((r) => r); // Validate default role is in available roles if ( defaultRole && availableRoles.length > 0 && !availableRoles.includes(defaultRole) ) { showToast("Default role must be one of the available roles", "error"); return; } if (redirectUris.length === 0) { showToast("At least one redirect URI is required", "error"); return; } const isEdit = !!editClientId; const url = isEdit ? `/api/admin/clients/${encodeURIComponent(editClientId)}` : "/api/admin/clients"; const method = isEdit ? "PUT" : "POST"; try { const response = await fetch(url, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ name, logoUrl, description, redirectUris, availableRoles: availableRolesText.trim() ? availableRoles : null, defaultRole: defaultRole || undefined, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to save client"); } clientModal.classList.remove("active"); // If creating a new client, show the credentials in modal if (!isEdit) { const result = await response.json(); if ( result.client && result.client.clientId && result.client.clientSecret ) { const secretModal = document.getElementById( "secretModal", ) as HTMLElement; const generatedClientId = document.getElementById( "generatedClientId", ) as HTMLElement; const generatedSecret = document.getElementById( "generatedSecret", ) as HTMLElement; if (generatedClientId && generatedSecret && secretModal) { generatedClientId.textContent = result.client.clientId; generatedSecret.textContent = result.client.clientSecret; secretModal.classList.add("active"); } } } else { showToast("Client updated successfully"); } await loadClients(); } catch (error) { console.error("Failed to save client:", error); showToast( `Failed to ${isEdit ? "update" : "create"} client: ${error instanceof Error ? error.message : "Unknown error"}`, "error", ); } }); (window as any).regenerateSecret = async (clientId: string, event?: Event) => { const btn = event?.target as HTMLButtonElement | undefined; // Double-click confirmation pattern (same as delete) if (btn?.dataset.confirmState === "pending") { // Second click - execute regenerate delete btn.dataset.confirmState; btn.disabled = true; btn.textContent = "regenerating..."; try { const response = await fetch( `/api/admin/clients/${encodeURIComponent(clientId)}/secret`, { method: "POST", headers: { Authorization: `Bearer ${token}`, }, }, ); if (!response.ok) { throw new Error("Failed to regenerate secret"); } const data = await response.json(); // Show the secret in modal const secretModal = document.getElementById("secretModal") as HTMLElement; const generatedClientId = document.getElementById( "generatedClientId", ) as HTMLElement; const generatedSecret = document.getElementById( "generatedSecret", ) as HTMLElement; if (generatedClientId && generatedSecret && secretModal) { generatedClientId.textContent = clientId; generatedSecret.textContent = data.clientSecret; secretModal.classList.add("active"); } btn.disabled = false; btn.textContent = "regenerate secret"; } catch (error) { console.error("Failed to regenerate secret:", error); showToast( "Failed to regenerate client secret. Please try again.", "error", ); btn.disabled = false; btn.textContent = "regenerate secret"; } } else { // First click - set pending state if (btn) { const originalText = btn.textContent; btn.dataset.confirmState = "pending"; btn.textContent = "you sure?"; // Reset after 3 seconds if not confirmed setTimeout(() => { if (btn.dataset.confirmState === "pending") { delete btn.dataset.confirmState; btn.textContent = originalText; } }, 3000); } } }; (window as any).revokeUserPermission = async ( clientId: string, username: string, event?: Event, ) => { const btn = event?.target as HTMLButtonElement | undefined; // Double-click confirmation pattern if (btn?.dataset.confirmState === "pending") { // Second click - execute revoke delete btn.dataset.confirmState; btn.disabled = true; btn.textContent = "revoking..."; try { const response = await fetch( `/api/admin/apps/${encodeURIComponent(clientId)}/users/${encodeURIComponent(username)}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}`, }, }, ); if (!response.ok) { throw new Error("Failed to revoke permission"); } // Reload the client details const detailsDiv = document.getElementById( `details-${encodeURIComponent(clientId)}`, ); if (detailsDiv) { detailsDiv.dataset.loaded = "false"; } const card = document.querySelector( `[data-client-id="${clientId}"]`, ) as HTMLElement; if (card) { card.classList.remove("expanded"); } await loadClients(); } catch (error) { console.error("Failed to revoke permission:", error); showToast("Failed to revoke permission. Please try again.", "error"); btn.disabled = false; btn.textContent = "revoke"; } } else { // First click - set pending state if (btn) { const originalText = btn.textContent; btn.dataset.confirmState = "pending"; btn.textContent = "you sure?"; // Reset after 3 seconds if not confirmed setTimeout(() => { if (btn.dataset.confirmState === "pending") { delete btn.dataset.confirmState; btn.textContent = originalText; } }, 3000); } } }; // Secret modal handlers const secretModal = document.getElementById("secretModal") as HTMLElement; const secretModalClose = document.getElementById( "secretModalClose", ) as HTMLButtonElement; const copyClientIdBtn = document.getElementById( "copyClientIdBtn", ) as HTMLButtonElement; const copySecretBtn = document.getElementById( "copySecretBtn", ) as HTMLButtonElement; secretModalClose?.addEventListener("click", () => { secretModal?.classList.remove("active"); }); copyClientIdBtn?.addEventListener("click", async () => { const generatedClientId = document.getElementById( "generatedClientId", ) as HTMLElement; if (generatedClientId) { try { await navigator.clipboard.writeText(generatedClientId.textContent || ""); const originalText = copyClientIdBtn.textContent; copyClientIdBtn.textContent = "copied! ✓"; setTimeout(() => { copyClientIdBtn.textContent = originalText; }, 2000); } catch (error) { console.error("Failed to copy:", error); showToast("Failed to copy to clipboard", "error"); } } }); copySecretBtn?.addEventListener("click", async () => { const generatedSecret = document.getElementById( "generatedSecret", ) as HTMLElement; if (generatedSecret) { try { await navigator.clipboard.writeText(generatedSecret.textContent || ""); const originalText = copySecretBtn.textContent; copySecretBtn.textContent = "copied! ✓"; setTimeout(() => { copySecretBtn.textContent = originalText; }, 2000); } catch (error) { console.error("Failed to copy:", error); showToast("Failed to copy to clipboard", "error"); } } }); // Close modals on escape key document.addEventListener("keydown", (e) => { if (e.key === "Escape") { clientModal?.classList.remove("active"); secretModal?.classList.remove("active"); } }); // Close modals on outside click clientModal?.addEventListener("click", (e) => { if (e.target === clientModal) { clientModal.classList.remove("active"); } }); secretModal?.addEventListener("click", (e) => { if (e.target === secretModal) { secretModal.classList.remove("active"); } }); checkAuth();