my own indieAuth provider!
indiko.dunkirk.sh/docs
indieauth
oauth2-server
1const token = localStorage.getItem("indiko_session");
2const footer = document.getElementById("footer") as HTMLElement;
3const usersList = document.getElementById("usersList") as HTMLElement;
4let currentUserId: number;
5
6// Check auth and display user
7async function checkAuth() {
8 if (!token) {
9 window.location.href = "/login";
10 return;
11 }
12
13 try {
14 const response = await fetch("/api/hello", {
15 headers: {
16 Authorization: `Bearer ${token}`,
17 },
18 });
19
20 if (response.status === 401 || response.status === 403) {
21 localStorage.removeItem("indiko_session");
22 window.location.href = "/login";
23 return;
24 }
25
26 const data = await response.json();
27 currentUserId = data.id;
28
29 footer.innerHTML = `admin • signed in as <strong><a href="/u/${data.username}">${data.username}</a></strong> • <a href="/login" id="logoutLink">sign out</a>
30 <div class="back-link"><a href="/">← back to dashboard</a></div>`;
31
32 // Handle logout
33 document
34 .getElementById("logoutLink")
35 ?.addEventListener("click", async (e) => {
36 e.preventDefault();
37 try {
38 await fetch("/auth/logout", {
39 method: "POST",
40 headers: {
41 Authorization: `Bearer ${token}`,
42 },
43 });
44 } catch {
45 // Ignore logout errors
46 }
47 localStorage.removeItem("indiko_session");
48 window.location.href = "/login";
49 });
50
51 // Check if admin
52 if (!data.isAdmin) {
53 window.location.href = "/";
54 return;
55 }
56
57 // Load users if admin
58 loadUsers();
59 } catch (error) {
60 console.error("Auth check failed:", error);
61 footer.textContent = "error loading user info";
62 usersList.innerHTML = '<div class="error">Failed to load users</div>';
63 }
64}
65
66async function loadUsers() {
67 try {
68 const response = await fetch("/api/users", {
69 headers: {
70 Authorization: `Bearer ${token}`,
71 },
72 });
73
74 if (!response.ok) {
75 throw new Error("Failed to load users");
76 }
77
78 const data = await response.json();
79
80 if (data.users.length === 0) {
81 usersList.innerHTML = '<div class="loading">No users found</div>';
82 return;
83 }
84
85 usersList.innerHTML = data.users
86 .map(
87 (user: {
88 id: number;
89 username: string;
90 name: string;
91 email: string | null;
92 photo: string | null;
93 status: string;
94 role: string;
95 isAdmin: boolean;
96 createdAt: number;
97 credentialCount: number;
98 }) => {
99 const createdDate = new Date(
100 user.createdAt * 1000,
101 ).toLocaleDateString();
102 const initials = user.username.substring(0, 2).toUpperCase();
103 const avatarContent = user.photo
104 ? `<img src="${user.photo}" alt="${user.username}" />`
105 : initials;
106 const isSelf = user.id === currentUserId;
107
108 return `
109 <div class="user-card ${user.status === "suspended" ? "user-suspended" : ""}" data-user-id="${user.id}">
110 <div class="user-avatar">${avatarContent}</div>
111 <div class="user-info">
112 <div class="user-name">${user.username}${isSelf ? " (you)" : ""}</div>
113 <div class="user-meta">
114 <span class="user-meta-item">${user.credentialCount} passkey${user.credentialCount !== 1 ? "s" : ""}</span>
115 <span class="user-meta-item">joined ${createdDate}</span>
116 ${user.email ? `<span class="user-meta-item">${user.email}</span>` : ""}
117 </div>
118 </div>
119 <div class="user-badges">
120 <span class="user-badge badge-status ${user.status}">${user.status}</span>
121 <span class="user-badge badge-role">${user.role}</span>
122 </div>
123 <div class="user-actions">
124 ${
125 !isSelf
126 ? user.status === "suspended"
127 ? `<button class="btn-edit" data-action="enable" data-user-id="${user.id}">enable</button>`
128 : `<button class="btn-disable" data-action="disable" data-user-id="${user.id}">disable</button>`
129 : ""
130 }
131 ${!isSelf ? `<button class="btn-delete" data-action="delete" data-user-id="${user.id}">delete</button>` : ""}
132 </div>
133 </div>
134 `;
135 },
136 )
137 .join("");
138
139 // Add event listeners for action buttons
140 document.querySelectorAll("button[data-action]").forEach((btn) => {
141 btn.addEventListener("click", handleUserAction);
142 });
143 } catch (error) {
144 console.error("Failed to load users:", error);
145 usersList.innerHTML = '<div class="error">Failed to load users</div>';
146 }
147}
148
149async function handleUserAction(e: Event) {
150 const btn = e.target as HTMLButtonElement;
151 const action = btn.dataset.action;
152 const userId = btn.dataset.userId;
153
154 if (!userId || !action) return;
155
156 // Check if already in confirmation state
157 if (btn.dataset.confirmState === "pending") {
158 // Second click - perform action
159 btn.dataset.confirmState = "";
160 btn.disabled = true;
161
162 try {
163 let endpoint = "";
164 let method = "POST";
165
166 if (action === "delete") {
167 endpoint = `/api/admin/users/${userId}/delete`;
168 method = "DELETE";
169 } else if (action === "disable") {
170 endpoint = `/api/admin/users/${userId}/disable`;
171 } else if (action === "enable") {
172 endpoint = `/api/admin/users/${userId}/enable`;
173 }
174
175 const response = await fetch(endpoint, {
176 method,
177 headers: {
178 Authorization: `Bearer ${token}`,
179 },
180 });
181
182 if (!response.ok) {
183 const error = await response.json();
184 throw new Error(error.error || "Failed to perform action");
185 }
186
187 // Reload users list
188 loadUsers();
189 } catch (error) {
190 console.error(`Failed to ${action} user:`, error);
191 alert(
192 `Failed to ${action} user: ${error instanceof Error ? error.message : "Unknown error"}`,
193 );
194 btn.disabled = false;
195 }
196 } else {
197 // First click - set confirmation state
198 const originalText = btn.textContent;
199 btn.dataset.confirmState = "pending";
200 btn.dataset.originalText = originalText || "";
201 btn.textContent = "you sure?";
202
203 // Reset after 3 seconds if not clicked again
204 setTimeout(() => {
205 if (btn.dataset.confirmState === "pending") {
206 btn.dataset.confirmState = "";
207 btn.textContent = btn.dataset.originalText || originalText;
208 }
209 }, 3000);
210 }
211}
212
213checkAuth();