const token = localStorage.getItem("indiko_session");
const footer = document.getElementById("footer") as HTMLElement;
const invitesList = document.getElementById("invitesList") as HTMLElement;
const createInviteBtn = document.getElementById(
"createInviteBtn",
) as HTMLButtonElement;
// Check auth and display user
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
`;
// Handle logout
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";
});
// Check if admin
if (!data.isAdmin) {
window.location.href = "/";
return;
}
// Load invites
loadInvites();
} catch (error) {
console.error("Auth check failed:", error);
footer.textContent = "error loading user info";
usersList.innerHTML = 'Failed to load users
';
}
}
async function createInvite() {
// Show the create invite modal
const modal = document.getElementById("createInviteModal");
if (modal) {
modal.style.display = "flex";
// Load apps for role assignment
await loadAppsForInvite();
}
}
async function loadAppsForInvite() {
try {
const response = await fetch("/api/admin/clients", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to load apps");
}
const data = await response.json();
const appRolesContainer = document.getElementById("appRolesContainer");
if (!appRolesContainer) return;
if (data.clients.length === 0) {
appRolesContainer.innerHTML =
'No pre-registered apps available
';
return;
}
appRolesContainer.innerHTML = data.clients
.filter((app: { isPreregistered: boolean }) => app.isPreregistered)
.map(
(app: {
id: number;
clientId: string;
name: string;
roles: string[];
}) => {
const roleOptions =
app.roles.length > 0
? app.roles
.map((role) => ``)
.join("")
: '';
const displayName = app.name || app.clientId;
return `
`;
},
)
.join("");
// Enable/disable role select when checkbox changes
const checkboxes = appRolesContainer.querySelectorAll(
'input[name="appRole"]',
);
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
const appId = target.value;
const roleSelect = appRolesContainer.querySelector(
`select.role-select[data-app-id="${appId}"]`,
) as HTMLSelectElement;
if (roleSelect) {
roleSelect.disabled = !target.checked;
if (!target.checked) {
roleSelect.value = "";
}
}
});
});
} catch (error) {
console.error("Failed to load apps:", error);
}
}
async function submitCreateInvite() {
const maxUsesInput = document.getElementById("maxUses") as HTMLInputElement;
const expiresAtInput = document.getElementById(
"expiresAt",
) as HTMLInputElement;
const noteInput = document.getElementById(
"inviteNote",
) as HTMLTextAreaElement;
const messageInput = document.getElementById(
"inviteMessage",
) as HTMLTextAreaElement;
const submitBtn = document.getElementById(
"submitInviteBtn",
) as HTMLButtonElement;
const maxUses = maxUsesInput.value ? parseInt(maxUsesInput.value, 10) : 1;
const expiresAt = expiresAtInput.value
? Math.floor(new Date(expiresAtInput.value).getTime() / 1000)
: null;
const note = noteInput.value.trim() || null;
const message = messageInput.value.trim() || null;
// Collect app roles
const appRolesContainer = document.getElementById("appRolesContainer");
const appRoles: Array<{ appId: number; role: string }> = [];
if (appRolesContainer) {
const checkedBoxes = appRolesContainer.querySelectorAll(
'input[name="appRole"]:checked',
);
checkedBoxes.forEach((checkbox) => {
const appId = parseInt((checkbox as HTMLInputElement).value, 10);
const roleSelect = appRolesContainer.querySelector(
`select.role-select[data-app-id="${appId}"]`,
) as HTMLSelectElement;
let role = "";
if (roleSelect && roleSelect.value) {
role = roleSelect.value;
}
if (role) {
appRoles.push({
appId,
role,
});
}
});
}
submitBtn.disabled = true;
submitBtn.textContent = "creating...";
try {
const response = await fetch("/api/invites/create", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
maxUses,
expiresAt,
note,
message,
appRoles: appRoles.length > 0 ? appRoles : undefined,
}),
});
if (!response.ok) {
throw new Error("Failed to create invite");
}
await loadInvites();
closeCreateInviteModal();
} catch (error) {
console.error("Failed to create invite:", error);
alert("Failed to create invite");
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "create invite";
}
}
function closeCreateInviteModal() {
const modal = document.getElementById("createInviteModal");
if (modal) {
modal.style.display = "none";
// Reset form
(document.getElementById("maxUses") as HTMLInputElement).value = "1";
(document.getElementById("expiresAt") as HTMLInputElement).value = "";
(document.getElementById("inviteNote") as HTMLTextAreaElement).value = "";
(document.getElementById("inviteMessage") as HTMLTextAreaElement).value =
"";
const appRolesContainer = document.getElementById("appRolesContainer");
if (appRolesContainer) {
appRolesContainer
.querySelectorAll('input[type="checkbox"]')
.forEach((input) => {
(input as HTMLInputElement).checked = false;
});
appRolesContainer.querySelectorAll("select").forEach((select) => {
(select as HTMLSelectElement).value = "";
(select as HTMLSelectElement).disabled = true;
});
}
}
}
// Expose functions to global scope for HTML onclick handlers
(window as any).submitCreateInvite = submitCreateInvite;
(window as any).closeCreateInviteModal = closeCreateInviteModal;
async function loadInvites() {
try {
const response = await fetch("/api/invites", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to load invites");
}
const data = await response.json();
if (data.invites.length === 0) {
invitesList.innerHTML =
'No invites created yet
';
return;
}
invitesList.innerHTML = data.invites
.map(
(invite: {
id: number;
code: string;
maxUses: number;
currentUses: number;
isExpired: boolean;
isFullyUsed: boolean;
expiresAt: number | null;
note: string | null;
message: string | null;
createdAt: number;
createdBy: string;
inviteUrl: string;
appRoles: Array<{
clientId: string;
name: string | null;
role: string;
}>;
usedBy: Array<{ username: string; usedAt: number }>;
}) => {
const createdDate = new Date(
invite.createdAt * 1000,
).toLocaleDateString();
let status = `${invite.currentUses}/${invite.maxUses} used`;
if (invite.isExpired) {
status += " (expired)";
} else if (invite.isFullyUsed) {
status += " (fully used)";
}
const expiryInfo = invite.expiresAt
? `Expires: ${new Date(invite.expiresAt * 1000).toLocaleString()}`
: "No expiry";
const roleInfo =
invite.appRoles.length > 0
? `App roles: ${invite.appRoles
.map((r) => {
const appName = r.name || r.clientId;
return `${appName} (${r.role})`;
})
.join(", ")}
`
: "";
const usedByInfo =
invite.usedBy.length > 0
? `Used by: ${invite.usedBy.map((u) => `${u.username} (${new Date(u.usedAt * 1000).toLocaleDateString()})`).join(", ")}
`
: "";
const noteInfo = invite.note
? `Internal note: ${invite.note}
`
: "";
const messageInfo = invite.message
? `Message to invitees: ${invite.message}
`
: "";
const isActive = !invite.isExpired && !invite.isFullyUsed;
return `
${invite.code}
Created by ${invite.createdBy} on ${createdDate} • ${status}
${expiryInfo}
${noteInfo}
${messageInfo}
${roleInfo}
${usedByInfo}
${invite.inviteUrl}
`;
},
)
.join("");
// Add copy button handlers
const copyButtons = invitesList.querySelectorAll(".btn-copy");
copyButtons.forEach((btn) => {
btn.addEventListener("click", async (e) => {
const button = e.target as HTMLButtonElement;
const url = button.dataset.inviteUrl;
if (!url) return;
try {
await navigator.clipboard.writeText(url);
const originalText = button.textContent;
button.textContent = "copied!";
setTimeout(() => {
button.textContent = originalText;
}, 2000);
} catch (error) {
console.error("Failed to copy:", error);
}
});
});
} catch (error) {
console.error("Failed to load invites:", error);
invitesList.innerHTML = 'Failed to load invites
';
}
}
checkAuth();
createInviteBtn.addEventListener("click", createInvite);
// Close modals on escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeCreateInviteModal();
closeEditInviteModal();
}
});
// Close modals on outside click
document.getElementById("createInviteModal")?.addEventListener("click", (e) => {
if (e.target === e.currentTarget) {
closeCreateInviteModal();
}
});
document.getElementById("editInviteModal")?.addEventListener("click", (e) => {
if (e.target === e.currentTarget) {
closeEditInviteModal();
}
});
let currentEditInviteId: number | null = null;
// Make editInvite globally available for onclick handler
(window as any).editInvite = async (inviteId: number) => {
try {
const response = await fetch("/api/invites", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to load invite");
}
const data = await response.json();
const invite = data.invites.find(
(inv: { id: number }) => inv.id === inviteId,
);
if (!invite) {
throw new Error("Invite not found");
}
currentEditInviteId = inviteId;
// Populate form
(document.getElementById("editMaxUses") as HTMLInputElement).value = String(
invite.maxUses,
);
(document.getElementById("editInviteNote") as HTMLTextAreaElement).value =
invite.note || "";
(
document.getElementById("editInviteMessage") as HTMLTextAreaElement
).value = invite.message || "";
// Handle expiration date
const expiresAtInput = document.getElementById(
"editExpiresAt",
) as HTMLInputElement;
if (invite.expiresAt) {
const date = new Date(invite.expiresAt * 1000);
const localDatetime = new Date(
date.getTime() - date.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16);
expiresAtInput.value = localDatetime;
} else {
expiresAtInput.value = "";
}
// Show modal
const modal = document.getElementById("editInviteModal");
if (modal) {
modal.style.display = "flex";
}
} catch (error) {
console.error("Failed to load invite:", error);
alert("Failed to load invite");
}
};
(window as any).submitEditInvite = async () => {
if (currentEditInviteId === null) return;
const maxUsesInput = document.getElementById(
"editMaxUses",
) as HTMLInputElement;
const expiresAtInput = document.getElementById(
"editExpiresAt",
) as HTMLInputElement;
const noteInput = document.getElementById(
"editInviteNote",
) as HTMLTextAreaElement;
const messageInput = document.getElementById(
"editInviteMessage",
) as HTMLTextAreaElement;
const submitBtn = document.getElementById(
"submitEditInviteBtn",
) as HTMLButtonElement;
const maxUses = maxUsesInput.value ? parseInt(maxUsesInput.value, 10) : null;
const expiresAt = expiresAtInput.value
? Math.floor(new Date(expiresAtInput.value).getTime() / 1000)
: null;
const note = noteInput.value.trim() || null;
const message = messageInput.value.trim() || null;
submitBtn.disabled = true;
submitBtn.textContent = "saving...";
try {
const response = await fetch(`/api/invites/${currentEditInviteId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ maxUses, expiresAt, note, message }),
});
if (!response.ok) {
throw new Error("Failed to update invite");
}
await loadInvites();
closeEditInviteModal();
} catch (error) {
console.error("Failed to update invite:", error);
alert("Failed to update invite");
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "save changes";
}
};
(window as any).closeEditInviteModal = () => {
const modal = document.getElementById("editInviteModal");
if (modal) {
modal.style.display = "none";
currentEditInviteId = null;
(document.getElementById("editMaxUses") as HTMLInputElement).value = "";
(document.getElementById("editExpiresAt") as HTMLInputElement).value = "";
(document.getElementById("editInviteNote") as HTMLTextAreaElement).value =
"";
(
document.getElementById("editInviteMessage") as HTMLTextAreaElement
).value = "";
}
};
(window as any).deleteInvite = async (inviteId: number, 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.textContent = "deleting...";
btn.disabled = true;
try {
const response = await fetch(`/api/invites/${inviteId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to delete invite");
}
await loadInvites();
} catch (error) {
console.error("Failed to delete invite:", error);
alert("Failed to delete invite");
btn.textContent = "delete";
btn.disabled = false;
}
} 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);
}
}
};