@rblez/authly 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/authly.js +3 -1
- package/dist/dashboard/app.js +182 -32
- package/dist/dashboard/authorize.html +117 -0
- package/dist/dashboard/index.html +192 -15
- package/dist/dashboard/styles.css +10 -0
- package/package.json +3 -2
- package/src/auth/index.js +98 -0
- package/src/commands/ext.js +107 -0
- package/src/commands/serve.js +218 -15
- package/src/generators/env.js +3 -2
- package/src/generators/migrations.js +33 -0
- package/src/integrations/supabase.js +156 -0
- package/src/lib/oauth.js +23 -1
- package/src/lib/supabase-api.js +152 -0
- package/src/lib/supabase-oauth.js +200 -0
package/bin/authly.js
CHANGED
|
@@ -3,13 +3,15 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { cmdServe } from "../src/commands/serve.js";
|
|
4
4
|
import { cmdInit } from "../src/commands/init.js";
|
|
5
5
|
import { cmdAudit } from "../src/commands/audit.js";
|
|
6
|
+
import { cmdExt } from "../src/commands/ext.js";
|
|
6
7
|
import chalk from "chalk";
|
|
7
8
|
|
|
8
9
|
const COMMANDS = {
|
|
9
10
|
serve: { description: "Start the local auth dashboard", handler: cmdServe },
|
|
10
11
|
init: { description: "Initialize authly in your project", handler: cmdInit },
|
|
12
|
+
ext: { description: "Manage extensions (add, remove)", handler: cmdExt },
|
|
11
13
|
audit: { description: "Check auth configuration for issues", handler: cmdAudit },
|
|
12
|
-
version: { description: "Show version", handler: () => console.log("0.
|
|
14
|
+
version: { description: "Show version", handler: () => console.log("0.3.0") },
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
async function main() {
|
package/dist/dashboard/app.js
CHANGED
|
@@ -44,9 +44,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
44
44
|
if (res.ok) {
|
|
45
45
|
statusText.textContent = "Connected";
|
|
46
46
|
statusDot.style.background = "#22c55e";
|
|
47
|
-
} else {
|
|
48
|
-
setDisconnected();
|
|
49
|
-
}
|
|
47
|
+
} else { setDisconnected(); }
|
|
50
48
|
} catch { setDisconnected(); }
|
|
51
49
|
}
|
|
52
50
|
|
|
@@ -55,6 +53,132 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
55
53
|
statusDot.style.background = "#ef4444";
|
|
56
54
|
}
|
|
57
55
|
|
|
56
|
+
// ── Auto-detect Supabase ──────────────────────────────
|
|
57
|
+
async function autoDetectSupabase() {
|
|
58
|
+
const scanStatus = document.getElementById("scanStatus");
|
|
59
|
+
const scanDetail = document.getElementById("scanDetail");
|
|
60
|
+
const scanAction = document.getElementById("scanAction");
|
|
61
|
+
const intStatus = document.getElementById("integrationStatus");
|
|
62
|
+
const intDetail = document.getElementById("integrationDetail");
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const scan = await api("/init/scan");
|
|
66
|
+
|
|
67
|
+
if (scan.detected && scan.url && scan.anonKey && scan.serviceKey) {
|
|
68
|
+
// Auto-connect
|
|
69
|
+
scanStatus.innerHTML = `<span style="color:#22c55e"><i class="ri-check-line"></i> Supabase credentials found — auto-connecting…</span>`;
|
|
70
|
+
const data = await api("/init/connect", { method: "POST" });
|
|
71
|
+
|
|
72
|
+
if (data.success) {
|
|
73
|
+
scanStatus.innerHTML = `<span style="color:#22c55e"><i class="ri-check-line"></i> Auto-connected to Supabase</span>`;
|
|
74
|
+
const ref = scan.projectRef || url.ref;
|
|
75
|
+
|
|
76
|
+
if (scanDetail) {
|
|
77
|
+
scanDetail.classList.remove("hidden");
|
|
78
|
+
scanDetail.innerHTML = `
|
|
79
|
+
<div style="color:#aaa;font-size:.78rem;line-height:1.7">
|
|
80
|
+
<div>URL: <code style="color:#c9c">${maskKey(scan.url)}</code></div>
|
|
81
|
+
<div>Project ref: <code style="color:#c9c">${scan.projectRef || "—"}</code></div>
|
|
82
|
+
<div>Found in: <span style="color:#22c55e">${scan.sources.join(", ")}</span></div>
|
|
83
|
+
<div>Framework: <code style="color:#c9c">${scan.framework || "unknown"}</code></div>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (intStatus) {
|
|
89
|
+
intStatus.innerHTML = `<span style="color:#22c55e"><i class="ri-check-line"></i> Connected to Supabase</span>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (intDetail) {
|
|
93
|
+
intDetail.classList.remove("hidden");
|
|
94
|
+
intDetail.innerHTML = `
|
|
95
|
+
<div style="color:#aaa;font-size:.78rem;line-height:1.7">
|
|
96
|
+
<div>URL: <code style="color:#c9c">${maskKey(scan.url)}</code></div>
|
|
97
|
+
<div>Project ref: <code style="color:#c9c">${scan.projectRef || "—"}</code></div>
|
|
98
|
+
<div>Framework: <code style="color:#c9c">${scan.framework || "unknown"}</code></div>
|
|
99
|
+
<div>Sources: <span style="color:#22c55e">${scan.sources.join(", ")}</span></div>
|
|
100
|
+
<div>Can connect: <span style="color:#22c55e">● yes</span></div>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
scanStatus.innerHTML = `<span style="color:#f87171">Auto-connect failed: ${data.error}</span>`;
|
|
106
|
+
showScanFallback(scan);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
showScanFallback(scan);
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
scanStatus.textContent = "Scan failed: " + e.message;
|
|
113
|
+
intStatus.innerHTML = `<span style="color:#888">Could not reach scan endpoint</span>`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function showScanFallback(scan) {
|
|
118
|
+
const scanStatus = document.getElementById("scanStatus");
|
|
119
|
+
const scanDetail = document.getElementById("scanDetail");
|
|
120
|
+
const scanAction = document.getElementById("scanAction");
|
|
121
|
+
const intStatus = document.getElementById("integrationStatus");
|
|
122
|
+
const intDetail = document.getElementById("integrationDetail");
|
|
123
|
+
|
|
124
|
+
let msg = "No Supabase credentials detected";
|
|
125
|
+
let color = "#f87171";
|
|
126
|
+
let icon = "ri-error-warning-line";
|
|
127
|
+
|
|
128
|
+
if (scan.detected) {
|
|
129
|
+
msg = "Partial credentials found — manual config needed";
|
|
130
|
+
color = "#f59e0b";
|
|
131
|
+
icon = "ri-alert-line";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
scanStatus.innerHTML = `<span style="color:${color}"><i class="${icon}"></i> ${msg}</span>`;
|
|
135
|
+
|
|
136
|
+
if (scanDetail) {
|
|
137
|
+
scanDetail.classList.remove("hidden");
|
|
138
|
+
const details = [];
|
|
139
|
+
if (scan.url) details.push(`<div>URL: <code style="color:#c9c">${maskKey(scan.url)}</code></div>`);
|
|
140
|
+
if (scan.anonKey) details.push(`<div>Anon key: <span style="color:#22c55e">found</span></div>`);
|
|
141
|
+
if (scan.serviceKey) details.push(`<div>Service key: <span style="color:#22c55e">found</span></div>`);
|
|
142
|
+
if (scan.projectRef) details.push(`<div>Ref: <code style="color:#c9c">${scan.projectRef}</code></div>`);
|
|
143
|
+
if (scan.sources.length) details.push(`<div>Sources: ${scan.sources.join(", ")}</div>`);
|
|
144
|
+
|
|
145
|
+
if (!details.length) details.push('<div style="color:#555">No env files contain Supabase credentials</div>');
|
|
146
|
+
|
|
147
|
+
scanDetail.innerHTML = `<div style="color:#aaa;font-size:.78rem;line-height:1.7">${details.join("")}</div>`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (scanAction) {
|
|
151
|
+
scanAction.classList.remove("hidden");
|
|
152
|
+
scanAction.innerHTML = `
|
|
153
|
+
<button class="btn btn--primary" id="connectBtn" style="font-size:.8rem;padding:8px 16px">
|
|
154
|
+
<i class="ri-plug-line"></i> Connect manually
|
|
155
|
+
</button>
|
|
156
|
+
`;
|
|
157
|
+
document.getElementById("connectBtn")?.addEventListener("click", () => {
|
|
158
|
+
document.querySelector('[data-section="init"]')?.click();
|
|
159
|
+
document.getElementById("initUrl")?.focus();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (intStatus) {
|
|
164
|
+
intStatus.innerHTML = `<span style="color:#888">Not connected — enter credentials in Init</span>`;
|
|
165
|
+
}
|
|
166
|
+
if (intDetail) {
|
|
167
|
+
intDetail.classList.add("hidden");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Connect (auto or manual) ──────────────────────
|
|
172
|
+
async function connectSupabase(body = {}) {
|
|
173
|
+
const data = await api("/init/connect", { method: "POST", body: JSON.stringify(body) });
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function maskKey(key) {
|
|
178
|
+
if (key.length < 30) return key;
|
|
179
|
+
return key.slice(0, 20) + "…" + key.slice(-8);
|
|
180
|
+
}
|
|
181
|
+
|
|
58
182
|
// ── Navigation ──────────────────────────────────────
|
|
59
183
|
for (const item of navItems) {
|
|
60
184
|
item.addEventListener("click", (e) => {
|
|
@@ -76,12 +200,49 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
76
200
|
if (id === "roles") loadRoles();
|
|
77
201
|
if (id === "env") loadEnv();
|
|
78
202
|
if (id === "migrate") loadMigrations();
|
|
79
|
-
if (id === "
|
|
203
|
+
if (id === "integration") loadIntegration();
|
|
80
204
|
}
|
|
81
205
|
|
|
82
|
-
|
|
83
|
-
|
|
206
|
+
async function loadIntegration() {
|
|
207
|
+
try {
|
|
208
|
+
const scan = await api("/init/scan");
|
|
209
|
+
const intStatus = document.getElementById("integrationStatus");
|
|
210
|
+
const intDetail = document.getElementById("integrationDetail");
|
|
211
|
+
|
|
212
|
+
if (scan.detected && scan.canConnect) {
|
|
213
|
+
intStatus.innerHTML = `<span style="color:#22c55e"><i class="ri-check-line"></i> Connected</span>`;
|
|
214
|
+
intDetail.classList.remove("hidden");
|
|
215
|
+
intDetail.innerHTML = `
|
|
216
|
+
<div style="color:#aaa;font-size:.78rem;line-height:1.7">
|
|
217
|
+
<div>URL: <code style="color:#c9c">${maskKey(scan.url || "")}</code></div>
|
|
218
|
+
<div>Project ref: <code style="color:#c9c">${scan.projectRef || "—"}</code></div>
|
|
219
|
+
<div>Sources: ${scan.sources.join(", ")}</div>
|
|
220
|
+
</div>
|
|
221
|
+
`;
|
|
222
|
+
} else {
|
|
223
|
+
intStatus.innerHTML = `<span style="color:#f87171"><i class="ri-close-line"></i> No connection</span>`;
|
|
224
|
+
intDetail.classList.remove("hidden");
|
|
225
|
+
intDetail.innerHTML = `<div style="color:#555">No Supabase credentials detected. Go to Init to configure.</div>`;
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Scan on load ──────────────────────────────────
|
|
231
|
+
autoDetectSupabase();
|
|
232
|
+
|
|
233
|
+
// Reconnect button
|
|
234
|
+
const reconnectBtn = document.getElementById("reconnectBtn");
|
|
235
|
+
if (reconnectBtn) {
|
|
236
|
+
reconnectBtn.addEventListener("click", async () => {
|
|
237
|
+
reconnectBtn.disabled = true;
|
|
238
|
+
reconnectBtn.textContent = "Scanning…";
|
|
239
|
+
await autoDetectSupabase();
|
|
240
|
+
reconnectBtn.disabled = false;
|
|
241
|
+
reconnectBtn.innerHTML = '<i class="ri-refresh-line"></i> Re-scan & reconnect';
|
|
242
|
+
});
|
|
243
|
+
}
|
|
84
244
|
|
|
245
|
+
// ── Manual connect form ──────────────────────────
|
|
85
246
|
const initForm = document.getElementById("initForm");
|
|
86
247
|
if (initForm) {
|
|
87
248
|
initForm.addEventListener("submit", async (e) => {
|
|
@@ -105,29 +266,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
105
266
|
btn.innerHTML = '<i class="ri-plug-line"></i> Connect & Configure';
|
|
106
267
|
|
|
107
268
|
if (data.success) {
|
|
108
|
-
showResult(result,
|
|
269
|
+
showResult(result, "Connected to Supabase. Framework: " + (data.framework || "unknown"), "ok");
|
|
109
270
|
showToast("Project connected successfully");
|
|
110
|
-
checkInitFiles();
|
|
111
271
|
checkHealth();
|
|
272
|
+
autoDetectSupabase();
|
|
112
273
|
} else {
|
|
113
274
|
showResult(result, data.error, "error");
|
|
114
275
|
}
|
|
115
276
|
});
|
|
116
277
|
}
|
|
117
278
|
|
|
118
|
-
async function checkInitFiles() {
|
|
119
|
-
try {
|
|
120
|
-
const res = await fetch("/api/config");
|
|
121
|
-
if (res.ok) {
|
|
122
|
-
const envCheck = await fetch("/api/health");
|
|
123
|
-
document.getElementById("envStatus").textContent = "checked";
|
|
124
|
-
document.getElementById("envStatus").className = "file-status file-status--ok";
|
|
125
|
-
document.getElementById("configStatus").textContent = "checked";
|
|
126
|
-
document.getElementById("configStatus").className = "file-status file-status--ok";
|
|
127
|
-
}
|
|
128
|
-
} catch {}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
279
|
// ── Users ───────────────────────────────────────────
|
|
132
280
|
window.loadUsers = async function() {
|
|
133
281
|
const body = document.getElementById("usersBody");
|
|
@@ -135,15 +283,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
135
283
|
try {
|
|
136
284
|
const data = await api("/users");
|
|
137
285
|
if (!data.success || !data.users?.length) {
|
|
138
|
-
body.innerHTML = '<tr><td colspan="4" class="text-muted">No users found. Connect Supabase
|
|
286
|
+
body.innerHTML = '<tr><td colspan="4" class="text-muted">No users found. Connect Supabase first.</td></tr>';
|
|
139
287
|
return;
|
|
140
288
|
}
|
|
141
289
|
body.innerHTML = data.users.map(u =>
|
|
142
290
|
`<tr>
|
|
143
|
-
<td style="font-family:var(--mono);font-size:.8rem">${
|
|
291
|
+
<td style="font-family:var(--mono);font-size:.8rem">${u.id.slice(0, 8)}…</td>
|
|
144
292
|
<td>${u.email ?? "—"}</td>
|
|
145
293
|
<td>${u.raw_user_meta_data?.role ?? "user"}</td>
|
|
146
|
-
<td class="text-muted">${
|
|
294
|
+
<td class="text-muted">${new Date(u.created_at).toLocaleDateString()}</td>
|
|
147
295
|
</tr>`
|
|
148
296
|
).join("");
|
|
149
297
|
} catch {
|
|
@@ -160,16 +308,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
160
308
|
const data = await api("/providers");
|
|
161
309
|
if (!data.providers) return;
|
|
162
310
|
|
|
163
|
-
container.innerHTML = data.providers.map(p =>
|
|
164
|
-
|
|
165
|
-
|
|
311
|
+
container.innerHTML = data.providers.map(p => {
|
|
312
|
+
const name = p.name === "magiclink" ? "Magic Link" : p.name;
|
|
313
|
+
const iconUrl = `https://cdn.simpleicons.org/${p.name === "magiclink" ? "resend" : p.name}/fff`;
|
|
314
|
+
return `<div class="provider-card">
|
|
315
|
+
<img src="${iconUrl}" width="20" height="20" alt="${name}" class="provider-icon-img" />
|
|
166
316
|
<div class="provider-info">
|
|
167
|
-
<div class="provider-name">${
|
|
168
|
-
<div class="provider-scopes"
|
|
317
|
+
<div class="provider-name">${name}</div>
|
|
318
|
+
<div class="provider-scopes">${p.scopes || ""}</div>
|
|
169
319
|
</div>
|
|
170
|
-
<span class="provider-status ${p.enabled ? "enabled" : "disabled"}">${p.enabled ? "
|
|
171
|
-
</div
|
|
172
|
-
).join("");
|
|
320
|
+
<span class="provider-status ${p.enabled ? "enabled" : "disabled"}">${p.enabled ? "Active" : "Off"}</span>
|
|
321
|
+
</div>`;
|
|
322
|
+
}).join("");
|
|
173
323
|
};
|
|
174
324
|
|
|
175
325
|
// ── Roles ───────────────────────────────────────────
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Authly — Authorize</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css">
|
|
9
|
+
<style>
|
|
10
|
+
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
11
|
+
.auth-container { width: 100%; max-width: 420px; padding: 20px; }
|
|
12
|
+
.auth-card {
|
|
13
|
+
background: #0a0a0a; border: 1px solid #1a1a1a; border-radius: 12px;
|
|
14
|
+
padding: 32px 24px; text-align: center;
|
|
15
|
+
}
|
|
16
|
+
.auth-card h1 { font-size: 1.3rem; color: #fff; margin: 0 0 4px; }
|
|
17
|
+
.auth-card p { color: #888; font-size: .85rem; margin: 0 0 12px; }
|
|
18
|
+
.section-label {
|
|
19
|
+
font-size: .7rem; color: #555; text-transform: uppercase;
|
|
20
|
+
letter-spacing: 1px; margin: 16px 0 8px; text-align: left;
|
|
21
|
+
}
|
|
22
|
+
.provider-btn {
|
|
23
|
+
display: flex; align-items: center; gap: 12px; width: 100%;
|
|
24
|
+
padding: 12px 16px; margin-bottom: 8px; border: 1px solid #222;
|
|
25
|
+
border-radius: 8px; background: #111; color: #fff;
|
|
26
|
+
font-size: .9rem; cursor: pointer; transition: border-color .2s;
|
|
27
|
+
}
|
|
28
|
+
.provider-btn:hover { border-color: #444; }
|
|
29
|
+
.provider-btn.disabled { opacity: .4; pointer-events: none; }
|
|
30
|
+
.provider-btn img { width: 20px; height: 20px; flex-shrink: 0; }
|
|
31
|
+
.provider-btn .label { flex: 1; text-align: left; text-transform: capitalize; }
|
|
32
|
+
.provider-btn .status { font-size: .7rem; color: #555; text-transform: uppercase; }
|
|
33
|
+
.platform-connect {
|
|
34
|
+
display: flex; align-items: center; gap: 12px; width: 100%;
|
|
35
|
+
padding: 12px 16px; margin-bottom: 8px; border: 1px solid #1d355e;
|
|
36
|
+
border-radius: 8px; background: #0d1b3e; color: #58a6ff;
|
|
37
|
+
font-size: .9rem; cursor: pointer; transition: border-color .2s;
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
}
|
|
40
|
+
.platform-connect:hover { border-color: #58a6ff; }
|
|
41
|
+
.platform-connect.connected { border-color: #22c55e44; background: #0a1a0a; color: #22c55e; }
|
|
42
|
+
.back-link {
|
|
43
|
+
display: inline-block; margin-top: 16px; color: #555;
|
|
44
|
+
font-size: .8rem; text-decoration: none;
|
|
45
|
+
}
|
|
46
|
+
.back-link:hover { color: #aaa; }
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<div class="auth-container">
|
|
51
|
+
<div class="auth-card">
|
|
52
|
+
<h1><i class="ri-shield-keyhole-line"></i> Sign in</h1>
|
|
53
|
+
<p>Connect your Supabase project to enable authentication</p>
|
|
54
|
+
|
|
55
|
+
<!-- Platform: Connect Supabase (OAuth to Supabase API) -->
|
|
56
|
+
<div class="section-label">Platform connection</div>
|
|
57
|
+
<a href="/api/auth/supabase/authorize" id="supabasePlatformBtn" class="platform-connect">
|
|
58
|
+
<img src="https://cdn.simpleicons.org/supabase/fff" width="20" height="20" alt="Supabase" />
|
|
59
|
+
<span class="label">Connect Supabase</span>
|
|
60
|
+
<span class="status" id="sbStatus">Not connected</span>
|
|
61
|
+
</a>
|
|
62
|
+
|
|
63
|
+
<!-- Regular OAuth providers -->
|
|
64
|
+
<div class="section-label">App authentication</div>
|
|
65
|
+
<div id="providerList"></div>
|
|
66
|
+
</div>
|
|
67
|
+
<a href="/" class="back-link"><i class="ri-arrow-left-line"></i> Back to dashboard</a>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
async function loadProviders() {
|
|
72
|
+
const container = document.getElementById("providerList");
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch("/api/providers");
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (!data.providers) { container.innerHTML = "<p style='color:#555'>—</p>"; return; }
|
|
77
|
+
|
|
78
|
+
container.innerHTML = data.providers.map(p => {
|
|
79
|
+
const name = p.name === "magiclink" ? "Magic Link" : p.name;
|
|
80
|
+
const iconUrl = `https://cdn.simpleicons.org/${p.name === "magiclink" ? "resend" : p.name}/fff`;
|
|
81
|
+
const cls = p.enabled ? "" : "disabled";
|
|
82
|
+
return `<div class="provider-btn ${cls}" data-provider="${p.name}" data-enabled="${p.enabled}">
|
|
83
|
+
<img src="${iconUrl}" alt="${name}" />
|
|
84
|
+
<span class="label">${name}</span>
|
|
85
|
+
<span class="status">${p.enabled ? "enabled" : "not configured"}</span>
|
|
86
|
+
</div>`;
|
|
87
|
+
}).join("");
|
|
88
|
+
|
|
89
|
+
// Attach click handlers
|
|
90
|
+
container.querySelectorAll(".provider-btn[data-enabled='true']").forEach(el => {
|
|
91
|
+
el.addEventListener("click", () => {
|
|
92
|
+
const provider = el.dataset.provider;
|
|
93
|
+
window.location.href = `/api/auth/${provider}/authorize`;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
container.innerHTML = "<p style='color:#555'>Failed to load providers</p>";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if Supabase is already connected
|
|
102
|
+
async function checkSupabaseConnected() {
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch("/api/health");
|
|
105
|
+
if (res.ok) {
|
|
106
|
+
document.getElementById("sbStatus").textContent = "Connected";
|
|
107
|
+
const btn = document.getElementById("supabasePlatformBtn");
|
|
108
|
+
btn.classList.add("connected");
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
loadProviders();
|
|
114
|
+
checkSupabaseConnected();
|
|
115
|
+
</script>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|