@rblez/authly 0.4.1 → 0.5.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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Roles section — list roles and role assignments.
3
+ */
4
+
5
+ import { api } from "../api.js";
6
+ import { renderLoading, renderError, renderSuccess } from "../components/states.js";
7
+ import { showToast } from "../components/toast.js";
8
+
9
+ export async function loadRoles(sectionContainer) {
10
+ renderLoading(sectionContainer, "Loading roles…");
11
+
12
+ try {
13
+ const data = await api("/api/roles");
14
+ const roles = data.roles || [];
15
+
16
+ if (!roles.length) {
17
+ renderSuccess(sectionContainer, `
18
+ <div style="display:flex;justify-content:space-between;align-items:center">
19
+ <div class="text-muted">No roles defined.</div>
20
+ <button id="addRoleBtn" class="btn btn--primary btn--sm"><i class="ri-add-line"></i> Add role</button>
21
+ </div>
22
+ `);
23
+ document.getElementById("addRoleBtn")?.addEventListener("click", createRoleInline);
24
+ return;
25
+ }
26
+
27
+ const html = `
28
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
29
+ <div style="display:flex;gap:6px;flex-wrap:wrap">
30
+ ${roles.map((r) => `<span class="role-chip">${r.name}</span>`).join("")}
31
+ </div>
32
+ <button id="addRoleBtn" class="btn btn--primary btn--sm"><i class="ri-add-line"></i> Add role</button>
33
+ </div>
34
+ `;
35
+
36
+ renderSuccess(sectionContainer, html);
37
+ document.getElementById("addRoleBtn")?.addEventListener("click", createRoleInline);
38
+ } catch (e) {
39
+ renderError(sectionContainer, `Failed to load roles: ${e.message}`);
40
+ }
41
+ }
42
+
43
+ async function createRoleInline() {
44
+ const name = prompt("Role name (e.g. editor):");
45
+ if (!name) return;
46
+
47
+ try {
48
+ const data = await api("/api/roles", {
49
+ method: "POST",
50
+ body: JSON.stringify({ name, description: "Custom role" }),
51
+ });
52
+ if (data.success) {
53
+ showToast(`Role '${name}' created`, "ok");
54
+ loadRoles(document.querySelector('[data-section="roles"]') || document.getElementById("roles"));
55
+ } else {
56
+ showToast(data.error || "Failed", "error");
57
+ }
58
+ } catch (e) {
59
+ showToast(e.message, "error");
60
+ }
61
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Supabase integration section.
3
+ * Handles auto-config via scan or OAuth flow.
4
+ */
5
+
6
+ import { api } from "../api.js";
7
+ import { state, setState } from "../state.js";
8
+ import { renderLoading, renderError, renderSuccess } from "../components/states.js";
9
+ import { showToast } from "../components/toast.js";
10
+
11
+ export async function loadSupabase(sectionContainer) {
12
+ renderLoading(sectionContainer, "Checking Supabase connection…");
13
+
14
+ try {
15
+ const status = await api("/api/supabase/status");
16
+
17
+ if (status.connected) {
18
+ setState("supabase.connected", true);
19
+ setState("supabase.project", status.project);
20
+ setState("supabase.scannedFrom", status.scannedFrom);
21
+
22
+ const content = `
23
+ <div class="supabase-connected">
24
+ <i class="ri-check-line" style="color:#22c55e;font-size:1.2rem"></i>
25
+ <h4>Connected to Supabase</h4>
26
+ <p class="text-muted">Project: <strong>${status.project || "—"}</strong></p>
27
+ <p class="text-muted">Source: ${status.scannedFrom || "environment"}</p>
28
+ <div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
29
+ <button id="disconnectBtn" class="btn btn--sm" style="background:#444;color:#fff">
30
+ <i class="ri-plug-line"></i> Disconnect
31
+ </button>
32
+ </div>
33
+ </div>
34
+ `;
35
+ renderSuccess(sectionContainer, content);
36
+
37
+ document.getElementById("disconnectBtn")?.addEventListener("click", () => {
38
+ showToast("Disconnect not yet implemented", "info");
39
+ });
40
+ } else {
41
+ setState("supabase.connected", false);
42
+ showConnectFlow(sectionContainer);
43
+ }
44
+ } catch (e) {
45
+ renderError(sectionContainer, `Failed to check connection: ${e.message}`);
46
+ }
47
+ }
48
+
49
+ async function showConnectFlow(container) {
50
+ container.innerHTML = `
51
+ <div class="supabase-connect">
52
+ <h4>Connect to Supabase</h4>
53
+ <p class="text-muted">Authly can find your credentials automatically, or you can connect via OAuth.</p>
54
+ <div id="supabaseFlowAction" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
55
+ <button id="startScanBtn" class="btn btn--primary btn--sm">
56
+ <i class="ri-search-eye-line"></i> Scan project files
57
+ </button>
58
+ </div>
59
+ <div id="supabaseFlowResult"></div>
60
+ </div>
61
+ `;
62
+
63
+ const scanBtn = document.getElementById("startScanBtn");
64
+ scanBtn?.addEventListener("click", startScan);
65
+ }
66
+
67
+ async function startScan() {
68
+ const actionDiv = document.getElementById("supabaseFlowAction");
69
+ const resultDiv = document.getElementById("supabaseFlowResult");
70
+
71
+ actionDiv.innerHTML = '<span class="text-muted"><i class="ri-loader-4-line spinner"></i> Scanning project files…</span>';
72
+ resultDiv.innerHTML = "";
73
+
74
+ try {
75
+ const scan = await api("/api/supabase/scan");
76
+
77
+ if (scan.found) {
78
+ // Found all credentials
79
+ const fields = scan.fields || [];
80
+ actionDiv.innerHTML = "";
81
+ resultDiv.innerHTML = `
82
+ <div class="state state--success">
83
+ <p>Found ${fields.length} credentials in your project files.</p>
84
+ <button id="useCredsBtn" class="btn btn--sm btn--primary" style="margin-top:8px">
85
+ <i class="ri-check-line"></i> Use these credentials
86
+ </button>
87
+ </div>
88
+ `;
89
+ document.getElementById("useCredsBtn")?.addEventListener("click", () => {
90
+ applyScanResult(scan);
91
+ });
92
+ } else {
93
+ // Nothing found → offer OAuth
94
+ const fields = scan.fields || [];
95
+ actionDiv.innerHTML = `
96
+ <button id="oauthBtn" class="btn btn--sm" style="background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff">
97
+ <i class="ri-plug-line"></i> Connect with Supabase OAuth
98
+ </button>
99
+ `;
100
+ resultDiv.innerHTML = `
101
+ <div class="state state--error">
102
+ <p>Missing: <code>${fields.join(", ")}</code></p>
103
+ <p class="text-muted">Connect via Supabase Platform OAuth instead.</p>
104
+ </div>
105
+ `;
106
+ document.getElementById("oauthBtn")?.addEventListener("click", redirectToOAuth);
107
+ }
108
+ } catch (e) {
109
+ actionDiv.innerHTML = "";
110
+ resultDiv.innerHTML = `<div class="state state--error"><p>Scan failed: ${e.message}</p></div>`;
111
+ }
112
+ }
113
+
114
+ async function applyScanResult(scan) {
115
+ const actionDiv = document.getElementById("supabaseFlowAction");
116
+ const resultDiv = document.getElementById("supabaseFlowResult");
117
+ actionDiv.innerHTML = '<span class="text-muted">Saving credentials…</span>';
118
+
119
+ try {
120
+ await api("/api/config", {
121
+ method: "POST",
122
+ body: JSON.stringify({
123
+ type: "supabase",
124
+ fields: scan.fields,
125
+ }),
126
+ });
127
+ resultDiv.innerHTML = `<div class="state state--success"><p><i class="ri-check-line"></i> Credentials applied. Reload section to verify.</p></div>`;
128
+ showToast("Supabase credentials saved", "ok");
129
+ setState("supabase.connected", true);
130
+ } catch (e) {
131
+ resultDiv.innerHTML = `<div class="state state--error"><p>Failed to save: ${e.message}</p></div>`;
132
+ }
133
+ }
134
+
135
+ function redirectToOAuth() {
136
+ const actionDiv = document.getElementById("supabaseFlowAction");
137
+ actionDiv.innerHTML = '<span class="text-muted"><i class="ri-external-link-line"></i> Redirecting to Supabase…</span>';
138
+ // Redirect the entire browser to Supabase OAuth
139
+ window.location.href = `${window.location.origin}/api/auth/supabase/authorize`;
140
+ }
141
+
142
+ export async function checkSupabaseStatus() {
143
+ try {
144
+ const status = await api("/api/supabase/status");
145
+ setState("supabase.connected", !!status.connected);
146
+ setState("supabase.project", status.project || null);
147
+ setState("supabase.scannedFrom", status.scannedFrom || null);
148
+ } catch {
149
+ setState("supabase.connected", false);
150
+ }
151
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Users section — list users and manage roles.
3
+ */
4
+
5
+ import { api } from "../api.js";
6
+ import { renderLoading, renderError, renderSuccess } from "../components/states.js";
7
+ import { showToast } from "../components/toast.js";
8
+
9
+ export async function loadUsers(sectionContainer) {
10
+ renderLoading(sectionContainer, "Loading users…");
11
+
12
+ try {
13
+ const data = await api("/api/users");
14
+ const users = data.users || [];
15
+
16
+ if (!users.length) {
17
+ renderSuccess(sectionContainer, '<div class="text-muted">No users found.</div>');
18
+ return;
19
+ }
20
+
21
+ const html = `
22
+ <div class="table-wrapper">
23
+ <table class="data-table">
24
+ <thead><tr><th>ID</th><th>Email</th><th>Role</th><th>Created</th><th>Roles</th></tr></thead>
25
+ <tbody>
26
+ ${users.map((u) => `
27
+ <tr>
28
+ <td style="font-family:var(--mono);font-size:.8rem">${u.id?.slice?.(0, 8) || "—" }…</td>
29
+ <td>${u.email || "—"}</td>
30
+ <td>${u.role || "user"}</td>
31
+ <td class="text-muted">${u.created_at ? new Date(u.created_at).toLocaleDateString() : "—"}</td>
32
+ <td><button class="btn btn--sm manage-roles-btn" data-user-id="${u.id}" style="font-size:.7rem">Manage roles</button></td>
33
+ </tr>
34
+ `).join("")}
35
+ </tbody>
36
+ </table>
37
+ </div>
38
+ <div id="userRolePanel" class="hidden" style="margin-top:12px"></div>
39
+ `;
40
+
41
+ renderSuccess(sectionContainer, html);
42
+
43
+ sectionContainer.querySelectorAll(".manage-roles-btn").forEach((btn) => {
44
+ btn.addEventListener("click", () => showRolePanel(btn.dataset.userId));
45
+ });
46
+ } catch (e) {
47
+ renderError(sectionContainer, `Failed to load users: ${e.message}`);
48
+ }
49
+ }
50
+
51
+ async function showRolePanel(userId) {
52
+ const panel = document.getElementById("userRolePanel");
53
+ panel.classList.remove("hidden");
54
+ panel.innerHTML = 'Loading…';
55
+
56
+ try {
57
+ const data = await api(`/api/users/${userId}/roles`);
58
+ const roles = data.roles || [];
59
+ const availableRoles = ["admin", "user", "guest"];
60
+
61
+ panel.innerHTML = `
62
+ <div style="padding:8px 12px;background:#111;border-radius:6px">
63
+ <div style="font-size:.8rem;color:#888;margin-bottom:8px">User: ${userId.slice(0, 8)}…</div>
64
+ <div style="display:flex;gap:6px;flex-wrap:wrap">
65
+ ${availableRoles.map((r) => `
66
+ <button class="btn btn--sm ${roles.includes(r) ? "" : "btn--primary"} role-toggle-btn" data-role="${r}" data-user-id="${userId}" data-active="${roles.includes(r)}">
67
+ ${r} ${roles.includes(r) ? "✓" : ""}
68
+ </button>
69
+ `).join("")}
70
+ </div>
71
+ </div>
72
+ `;
73
+
74
+ panel.querySelectorAll(".role-toggle-btn").forEach((btn) => {
75
+ btn.addEventListener("click", async () => {
76
+ const role = btn.dataset.role;
77
+ const uid = btn.dataset.userId;
78
+ const isActive = btn.dataset.active === "true";
79
+
80
+ try {
81
+ if (isActive) {
82
+ await api(`/api/users/${uid}/roles/${role}`, { method: "DELETE" });
83
+ } else {
84
+ await api(`/api/users/${uid}/roles`, { method: "POST", body: JSON.stringify({ role }) });
85
+ }
86
+ showToast(`Role '${role}' ${isActive ? "removed" : "assigned"}`, "ok");
87
+ showRolePanel(uid);
88
+ } catch (e) {
89
+ showToast(e.message, "error");
90
+ }
91
+ });
92
+ });
93
+ } catch (e) {
94
+ panel.innerHTML = `<div class="text-muted">Failed to load roles: ${e.message}</div>`;
95
+ }
96
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Global state for the dashboard.
3
+ * Shared across sections so they don't re-fetch unnecessarily.
4
+ */
5
+
6
+ export const state = {
7
+ supabase: {
8
+ connected: false,
9
+ project: null,
10
+ scannedFrom: null, // "env" | "oauth" | null
11
+ },
12
+ providers: [],
13
+ users: [],
14
+ roles: [],
15
+ migrations: [],
16
+ keys: [],
17
+ config: {},
18
+ };
19
+
20
+ /**
21
+ * Update a slice of state and optionally re-render.
22
+ */
23
+ export function setState(path, value) {
24
+ const parts = path.split(".");
25
+ let obj = state;
26
+ for (let i = 0; i < parts.length - 1; i++) {
27
+ obj = obj[parts[i]];
28
+ }
29
+ obj[parts[parts.length - 1]] = value;
30
+ }