@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 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.1.0") },
14
+ version: { description: "Show version", handler: () => console.log("0.3.0") },
13
15
  };
14
16
 
15
17
  async function main() {
@@ -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 === "init") checkInitFiles();
203
+ if (id === "integration") loadIntegration();
80
204
  }
81
205
 
82
- // ── Init ────────────────────────────────────────────
83
- checkInitFiles();
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 &amp; Configure';
106
267
 
107
268
  if (data.success) {
108
- showResult(result, `Connected to Supabase via ${data.framework.name} (${data.framework.version})`, "ok");
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 in Init.</td></tr>';
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">${(u.id ?? "").slice(0, 8)}…</td>
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">${u.created_at ? new Date(u.created_at).toLocaleDateString() : "—"}</td>
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
- `<div class="provider-card">
165
- <i class="ri-${p.name === "google" ? "google" : p.name === "github" ? "github" : p.name === "discord" ? "discord" : "shield-keyhole"}-line"></i>
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">${p.name}</div>
168
- <div class="provider-scopes">scope: ${p.scopes}</div>
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 ? "Configured" : "Not configured"}</span>
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'>&mdash;</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>