@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.
- package/dist/dashboard/app.js +915 -397
- package/dist/dashboard/app.js.map +38 -0
- package/dist/dashboard/index.html +12 -49
- package/dist/dashboard/styles.css +63 -0
- package/package.json +3 -1
- package/src/commands/serve.js +490 -263
- package/src/dashboard/api.js +22 -0
- package/src/dashboard/components/states.js +16 -0
- package/src/dashboard/components/toast.js +12 -0
- package/src/dashboard/components/wizard.js +52 -0
- package/src/dashboard/index.js +124 -0
- package/src/dashboard/sections/apikeys.js +85 -0
- package/src/dashboard/sections/audit.js +38 -0
- package/src/dashboard/sections/mcp.js +30 -0
- package/src/dashboard/sections/migrations.js +84 -0
- package/src/dashboard/sections/providers.js +186 -0
- package/src/dashboard/sections/roles.js +61 -0
- package/src/dashboard/sections/supabase.js +151 -0
- package/src/dashboard/sections/users.js +96 -0
- package/src/dashboard/state.js +30 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": [
|
|
4
|
+
"dashboard/index.js",
|
|
5
|
+
"dashboard/api.js",
|
|
6
|
+
"dashboard/state.js",
|
|
7
|
+
"dashboard/components/toast.js",
|
|
8
|
+
"dashboard/sections/supabase.js",
|
|
9
|
+
"dashboard/sections/providers.js",
|
|
10
|
+
"dashboard/sections/migrations.js",
|
|
11
|
+
"dashboard/sections/users.js",
|
|
12
|
+
"dashboard/sections/roles.js",
|
|
13
|
+
"dashboard/sections/apikeys.js",
|
|
14
|
+
"dashboard/sections/audit.js",
|
|
15
|
+
"dashboard/sections/mcp.js",
|
|
16
|
+
"dashboard/components/states.js",
|
|
17
|
+
"dashboard/components/wizard.js"
|
|
18
|
+
],
|
|
19
|
+
"sourcesContent": [
|
|
20
|
+
"/**\n * Authly Dashboard — entry point.\n * Bundles all sections into a single app.js by esbuild.\n */\n\nimport { api, BASE } from \"./api.js\";\nimport { state, setState } from \"./state.js\";\nimport { showToast } from \"./components/toast.js\";\nimport { loadSupabase, checkSupabaseStatus } from \"./sections/supabase.js\";\nimport { loadProviders } from \"./sections/providers.js\";\nimport { loadMigrations } from \"./sections/migrations.js\";\nimport { loadUsers } from \"./sections/users.js\";\nimport { loadRoles } from \"./sections/roles.js\";\nimport { loadApiKeys } from \"./sections/apikeys.js\";\nimport { loadAudit } from \"./sections/audit.js\";\nimport { loadMCP } from \"./sections/mcp.js\";\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n const navItems = document.querySelectorAll(\".nav-item\");\n const sections = document.querySelectorAll(\".section\");\n const headerTitle = document.getElementById(\"headerTitle\");\n const sidebar = document.getElementById(\"sidebar\");\n const menuBtn = document.getElementById(\"menuBtn\");\n const statusText = document.getElementById(\"statusText\");\n const statusDot = document.querySelector(\".header__dot\");\n\n checkHealth();\n\n // ── Health ──────────────────────────────────────────\n async function checkHealth() {\n try {\n const res = await fetch(`${BASE}/health`);\n if (res.ok) {\n statusText.textContent = \"Connected\";\n if (statusDot) statusDot.style.background = \"#22c55e\";\n } else { setDisconnected(); }\n } catch { setDisconnected(); }\n }\n\n function setDisconnected() {\n statusText.textContent = \"Disconnected\";\n if (statusDot) statusDot.style.background = \"#ef4444\";\n }\n\n // ── Navigate ────────────────────────────────────────\n const sectionLoaders = {\n init: null,\n integration: loadSupabase,\n providers: loadProviders,\n ui: null,\n routes: null,\n roles: loadRoles,\n \"api-keys\": loadApiKeys,\n env: null,\n migrate: loadMigrations,\n users: loadUsers,\n mcp: loadMCP,\n audit: loadAudit,\n };\n\n for (const item of navItems) {\n item.addEventListener(\"click\", (e) => {\n e.preventDefault();\n switchSection(item.dataset.section);\n });\n }\n\n function switchSection(id) {\n for (const n of navItems) n.classList.remove(\"active\");\n document.querySelector(`[data-section=\"${id}\"]`)?.classList.add(\"active\");\n for (const sec of sections) sec.classList.toggle(\"hidden\", sec.id !== id);\n if (headerTitle) headerTitle.textContent = document.querySelector(`[data-section=\"${id}\"]`)?.textContent?.trim() ?? \"\";\n sidebar?.classList.remove(\"open\");\n\n const container = document.getElementById(id);\n const loader = sectionLoaders[id];\n if (loader && container) loader(container);\n }\n\n // ── Mobile menu ─────────────────────────────────────\n if (menuBtn) menuBtn.addEventListener(\"click\", () => sidebar?.classList.toggle(\"open\"));\n\n // ── Toggles ─────────────────────────────────────────\n document.querySelectorAll(\".toggle\")?.forEach((toggle) => {\n toggle.addEventListener(\"click\", () => {\n toggle.setAttribute(\"aria-checked\", String(toggle.getAttribute(\"aria-checked\") !== \"true\"));\n });\n });\n\n // ── Doc panel toggles ──────────────────────────────\n document.querySelectorAll(\".toggle-docs\")?.forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n const target = document.getElementById(btn.dataset.target);\n if (!target) return;\n target.classList.toggle(\"collapsed\");\n target.classList.toggle(\"expanded\");\n });\n });\n\n // ── Scaffold (UI section) ───────────────────────────\n document.querySelectorAll(\".scaffold-card\")?.forEach((card) => {\n card.addEventListener(\"click\", async () => {\n const type = card.dataset.scaffold;\n const preview = document.getElementById(\"scaffoldPreview\");\n const code = document.getElementById(\"scaffoldCode\");\n try {\n const data = await api(\"/api/scaffold/preview\", {\n method: \"POST\",\n body: JSON.stringify({ type }),\n });\n if (data.success && code && preview) {\n code.textContent = data.code;\n preview.classList.remove(\"hidden\");\n }\n } catch (e) {\n showToast(e.message, \"error\");\n }\n });\n });\n\n // ── Hash navigation ────────────────────────────────\n const hash = location.hash.replace(\"#\", \"\");\n if (hash && document.getElementById(hash)) switchSection(hash);\n});\n",
|
|
21
|
+
"/**\n * Base API client — all dashboard HTTP calls go through this.\n * Points to the hosted Railway instance.\n * No direct fetch() allowed in other files.\n */\n\nexport const BASE = \"https://authly.rblez.com\";\n\n/**\n * @param {string} path - API path (e.g. '/api/users')\n * @param {RequestInit} [options]\n * @returns {Promise<any>}\n */\nexport async function api(path, options = {}) {\n const res = await fetch(`${BASE}${path}`, {\n headers: { \"Content-Type\": \"application/json\" },\n ...options,\n });\n const data = await res.json();\n if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);\n return data;\n}\n",
|
|
22
|
+
"/**\n * Global state for the dashboard.\n * Shared across sections so they don't re-fetch unnecessarily.\n */\n\nexport const state = {\n supabase: {\n connected: false,\n project: null,\n scannedFrom: null, // \"env\" | \"oauth\" | null\n },\n providers: [],\n users: [],\n roles: [],\n migrations: [],\n keys: [],\n config: {},\n};\n\n/**\n * Update a slice of state and optionally re-render.\n */\nexport function setState(path, value) {\n const parts = path.split(\".\");\n let obj = state;\n for (let i = 0; i < parts.length - 1; i++) {\n obj = obj[parts[i]];\n }\n obj[parts[parts.length - 1]] = value;\n}\n",
|
|
23
|
+
"/**\n * Toast notification system.\n */\n\nexport function showToast(message, type = \"ok\") {\n const toast = document.getElementById(\"toast\");\n if (!toast) return;\n toast.textContent = message;\n toast.className = `toast toast--${type}`;\n toast.classList.remove(\"hidden\");\n setTimeout(() => toast.classList.add(\"hidden\"), 3500);\n}\n",
|
|
24
|
+
"/**\n * Supabase integration section.\n * Handles auto-config via scan or OAuth flow.\n */\n\nimport { api } from \"../api.js\";\nimport { state, setState } from \"../state.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\nimport { showToast } from \"../components/toast.js\";\n\nexport async function loadSupabase(sectionContainer) {\n renderLoading(sectionContainer, \"Checking Supabase connection…\");\n\n try {\n const status = await api(\"/api/supabase/status\");\n\n if (status.connected) {\n setState(\"supabase.connected\", true);\n setState(\"supabase.project\", status.project);\n setState(\"supabase.scannedFrom\", status.scannedFrom);\n\n const content = `\n <div class=\"supabase-connected\">\n <i class=\"ri-check-line\" style=\"color:#22c55e;font-size:1.2rem\"></i>\n <h4>Connected to Supabase</h4>\n <p class=\"text-muted\">Project: <strong>${status.project || \"—\"}</strong></p>\n <p class=\"text-muted\">Source: ${status.scannedFrom || \"environment\"}</p>\n <div style=\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap\">\n <button id=\"disconnectBtn\" class=\"btn btn--sm\" style=\"background:#444;color:#fff\">\n <i class=\"ri-plug-line\"></i> Disconnect\n </button>\n </div>\n </div>\n `;\n renderSuccess(sectionContainer, content);\n\n document.getElementById(\"disconnectBtn\")?.addEventListener(\"click\", () => {\n showToast(\"Disconnect not yet implemented\", \"info\");\n });\n } else {\n setState(\"supabase.connected\", false);\n showConnectFlow(sectionContainer);\n }\n } catch (e) {\n renderError(sectionContainer, `Failed to check connection: ${e.message}`);\n }\n}\n\nasync function showConnectFlow(container) {\n container.innerHTML = `\n <div class=\"supabase-connect\">\n <h4>Connect to Supabase</h4>\n <p class=\"text-muted\">Authly can find your credentials automatically, or you can connect via OAuth.</p>\n <div id=\"supabaseFlowAction\" style=\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap\">\n <button id=\"startScanBtn\" class=\"btn btn--primary btn--sm\">\n <i class=\"ri-search-eye-line\"></i> Scan project files\n </button>\n </div>\n <div id=\"supabaseFlowResult\"></div>\n </div>\n `;\n\n const scanBtn = document.getElementById(\"startScanBtn\");\n scanBtn?.addEventListener(\"click\", startScan);\n}\n\nasync function startScan() {\n const actionDiv = document.getElementById(\"supabaseFlowAction\");\n const resultDiv = document.getElementById(\"supabaseFlowResult\");\n\n actionDiv.innerHTML = '<span class=\"text-muted\"><i class=\"ri-loader-4-line spinner\"></i> Scanning project files…</span>';\n resultDiv.innerHTML = \"\";\n\n try {\n const scan = await api(\"/api/supabase/scan\");\n\n if (scan.found) {\n // Found all credentials\n const fields = scan.fields || [];\n actionDiv.innerHTML = \"\";\n resultDiv.innerHTML = `\n <div class=\"state state--success\">\n <p>Found ${fields.length} credentials in your project files.</p>\n <button id=\"useCredsBtn\" class=\"btn btn--sm btn--primary\" style=\"margin-top:8px\">\n <i class=\"ri-check-line\"></i> Use these credentials\n </button>\n </div>\n `;\n document.getElementById(\"useCredsBtn\")?.addEventListener(\"click\", () => {\n applyScanResult(scan);\n });\n } else {\n // Nothing found → offer OAuth\n const fields = scan.fields || [];\n actionDiv.innerHTML = `\n <button id=\"oauthBtn\" class=\"btn btn--sm\" style=\"background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff\">\n <i class=\"ri-plug-line\"></i> Connect with Supabase OAuth\n </button>\n `;\n resultDiv.innerHTML = `\n <div class=\"state state--error\">\n <p>Missing: <code>${fields.join(\", \")}</code></p>\n <p class=\"text-muted\">Connect via Supabase Platform OAuth instead.</p>\n </div>\n `;\n document.getElementById(\"oauthBtn\")?.addEventListener(\"click\", redirectToOAuth);\n }\n } catch (e) {\n actionDiv.innerHTML = \"\";\n resultDiv.innerHTML = `<div class=\"state state--error\"><p>Scan failed: ${e.message}</p></div>`;\n }\n}\n\nasync function applyScanResult(scan) {\n const actionDiv = document.getElementById(\"supabaseFlowAction\");\n const resultDiv = document.getElementById(\"supabaseFlowResult\");\n actionDiv.innerHTML = '<span class=\"text-muted\">Saving credentials…</span>';\n\n try {\n await api(\"/api/config\", {\n method: \"POST\",\n body: JSON.stringify({\n type: \"supabase\",\n fields: scan.fields,\n }),\n });\n resultDiv.innerHTML = `<div class=\"state state--success\"><p><i class=\"ri-check-line\"></i> Credentials applied. Reload section to verify.</p></div>`;\n showToast(\"Supabase credentials saved\", \"ok\");\n setState(\"supabase.connected\", true);\n } catch (e) {\n resultDiv.innerHTML = `<div class=\"state state--error\"><p>Failed to save: ${e.message}</p></div>`;\n }\n}\n\nfunction redirectToOAuth() {\n const actionDiv = document.getElementById(\"supabaseFlowAction\");\n actionDiv.innerHTML = '<span class=\"text-muted\"><i class=\"ri-external-link-line\"></i> Redirecting to Supabase…</span>';\n // Redirect the entire browser to Supabase OAuth\n window.location.href = `${window.location.origin}/api/auth/supabase/authorize`;\n}\n\nexport async function checkSupabaseStatus() {\n try {\n const status = await api(\"/api/supabase/status\");\n setState(\"supabase.connected\", !!status.connected);\n setState(\"supabase.project\", status.project || null);\n setState(\"supabase.scannedFrom\", status.scannedFrom || null);\n } catch {\n setState(\"supabase.connected\", false);\n }\n}\n",
|
|
25
|
+
"/**\n * Providers section — Google, GitHub, Discord.\n * Shows provider status with Setup wizard.\n */\n\nimport { api } from \"../api.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\nimport { openWizard } from \"../components/wizard.js\";\nimport { showToast } from \"../components/toast.js\";\n\nconst PROVIDERS = [\"google\", \"github\", \"discord\"];\n\nconst PROVIDER_LABELS = {\n google: \"Google\",\n github: \"GitHub\",\n discord: \"Discord\",\n};\n\nconst CALLBACK_URLS = {\n google: \"https://authly.rblez.com/api/auth/google/callback\",\n github: \"https://authly.rblez.com/api/auth/github/callback\",\n discord: \"https://authly.rblez.com/api/auth/discord/callback\",\n};\n\nconst DOCS_URLS = {\n google: \"https://console.cloud.google.com/apis/credentials\",\n github: \"https://github.com/settings/applications/new\",\n discord: \"https://discord.com/developers/applications\",\n};\n\nconst GUIDE_STEPS = {\n google: [\n \"Go to Google Cloud Console → APIs & Services → Credentials\",\n \"Create a project or select an existing one\",\n 'Click \"Create Credentials\" → OAuth 2.0 Client ID',\n \"Application type: Web application\",\n \"Add the callback URL below as an Authorized redirect URI\",\n ],\n github: [\n \"Go to GitHub Settings → Developer settings → OAuth Apps\",\n 'Click \"New OAuth App\"',\n \"Fill in Application name and Homepage URL\",\n \"Add the callback URL below as Authorization callback URL\",\n ],\n discord: [\n \"Go to Discord Developer Portal → Applications\",\n \"Create a New Application\",\n \"Go to OAuth2 → Redirects → Add Redirect\",\n \"Add the callback URL below\",\n ],\n};\n\nexport async function loadProviders(sectionContainer) {\n renderLoading(sectionContainer, \"Loading providers…\");\n\n try {\n const data = await api(\"/api/providers\");\n const providers = data.providers || [];\n\n const html = providers.map((p) => {\n const label = PROVIDER_LABELS[p.name] || p.name;\n const iconUrl = `https://cdn.simpleicons.org/${p.name}/fff`;\n const iconClass = p.enabled ? \"enabled\" : \"disabled\";\n return `<div class=\"provider-card\">\n <img src=\"${iconUrl}\" width=\"20\" height=\"20\" alt=\"${label}\" />\n <div class=\"provider-info\">\n <div class=\"provider-name\">${label}</div>\n <div class=\"provider-scopes\">${p.scopes || \"\"}</div>\n </div>\n <span class=\"provider-status ${iconClass}\">${p.enabled ? \"Enabled\" : \"Disabled\"}</span>\n ${p.enabled\n ? `<button class=\"btn btn--sm edit-provider-btn\" data-provider=\"${p.name}\">Edit</button>`\n : `<button class=\"btn btn--sm btn--primary setup-provider-btn\" data-provider=\"${p.name}\">Setup</button>`\n }\n </div>`;\n }).join(\"\");\n\n renderSuccess(sectionContainer, html);\n\n // Bind buttons\n sectionContainer.querySelectorAll(\".setup-provider-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => openProviderWizard(btn.dataset.provider));\n });\n sectionContainer.querySelectorAll(\".edit-provider-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => openProviderWizard(btn.dataset.provider, 1));\n });\n } catch (e) {\n renderError(sectionContainer, `Failed to load providers: ${e.message}`);\n }\n}\n\nfunction openProviderWizard(name, defaultTab = 0) {\n const label = PROVIDER_LABELS[name];\n const callbackUrl = CALLBACK_URLS[name];\n\n const tabs = [\n {\n label: \"Guide\",\n content: `\n <h4>${label} Setup Guide</h4>\n <ol style=\"color:#aaa;font-size:.85rem;line-height:2;padding-left:18px\">\n ${(GUIDE_STEPS[name] || []).map((s) => `<li>${s}</li>`).join(\"\")}\n </ol>\n <div style=\"margin-top:12px;padding:8px 12px;background:#111;border-radius:6px\">\n <div style=\"font-size:.75rem;color:#555;margin-bottom:4px\">Callback URL</div>\n <div style=\"display:flex;gap:6px;align-items:center\">\n <code id=\"cbUrl\" style=\"font-size:.8rem;color:#58a6ff;flex:1\">${callbackUrl}</code>\n <button class=\"btn btn--sm\" id=\"copyCbUrl\" title=\"Copy\">Copy</button>\n </div>\n </div>\n <div style=\"margin-top:8px\">\n <a href=\"${DOCS_URLS[name]}\" target=\"_blank\" rel=\"noopener\" class=\"btn btn--sm\" style=\"background:#222;color:#888;text-decoration:none\">\n Open ${label} Console <i class=\"ri-external-link-line\" style=\"font-size:.7rem\"></i>\n </a>\n </div>\n `,\n onShow: () => {\n document.getElementById(\"copyCbUrl\")?.addEventListener(\"click\", () => {\n navigator.clipboard.writeText(callbackUrl);\n showToast(\"Callback URL copied\", \"ok\");\n });\n },\n },\n {\n label: \"Enter Keys\",\n content: `\n <h4>${label} OAuth Credentials</h4>\n <div style=\"display:flex;flex-direction:column;gap:8px;margin-top:12px\">\n <input id=\"providerClientId\" placeholder=\"Client ID\" style=\"background:#000;border:1px solid #333;color:#fff;padding:8px 12px;border-radius:4px;font-size:.85rem\" />\n <input id=\"providerClientSecret\" type=\"password\" placeholder=\"Client Secret\" style=\"background:#000;border:1px solid #333;color:#fff;padding:8px 12px;border-radius:4px;font-size:.85rem\" />\n <div id=\"providerKeyResult\" class=\"result hidden\"></div>\n <button id=\"providerSaveKeys\" class=\"btn btn--primary btn--sm\">Validate & Save</button>\n </div>\n `,\n onShow: () => {\n document.getElementById(\"providerSaveKeys\")?.addEventListener(\"click\", async () => {\n const clientId = document.getElementById(\"providerClientId\").value.trim();\n const clientSecret = document.getElementById(\"providerClientSecret\").value.trim();\n if (!clientId || !clientSecret) return;\n\n const resultDiv = document.getElementById(\"providerKeyResult\");\n resultDiv.classList.remove(\"hidden\");\n resultDiv.innerHTML = \"Validating…\";\n\n try {\n const res = await api(`/api/providers/${name}/keys`, {\n method: \"POST\",\n body: JSON.stringify({ clientId, clientSecret }),\n });\n if (res.valid) {\n resultDiv.innerHTML = `<span style=\"color:#22c55e\">Keys validated and saved</span>`;\n showToast(`${label} provider configured`, \"ok\");\n } else {\n resultDiv.innerHTML = `<span style=\"color:#f87171\">Validation failed: ${res.error || \"invalid keys\"}</span>`;\n }\n } catch (e) {\n resultDiv.innerHTML = `<span style=\"color:#f87171\">${e.message}</span>`;\n }\n });\n },\n },\n {\n label: \"Test\",\n content: `\n <h4>Test ${label} Connection</h4>\n <p class=\"text-muted\" style=\"font-size:.8rem;margin:8px 0\">Make a test OAuth request to verify your configuration.</p>\n <pre id=\"providerTestResult\" class=\"code-block\" style=\"white-space:pre-wrap;font-size:.75rem;color:#888\"></pre>\n <button id=\"providerTestBtn\" class=\"btn btn--sm\">Run Test</button>\n `,\n onShow: () => {\n document.getElementById(\"providerTestBtn\")?.addEventListener(\"click\", async () => {\n const output = document.getElementById(\"providerTestResult\");\n output.textContent = \"Testing…\";\n try {\n const data = await api(`/api/providers/${name}/test`);\n output.textContent = JSON.stringify(data, null, 2);\n } catch (e) {\n output.textContent = e.message;\n }\n });\n },\n },\n ];\n\n openWizard({ title: `${label} Setup`, tabs, defaultTab });\n}\n",
|
|
26
|
+
"/**\n * Migrations section — list and run SQL migrations.\n */\n\nimport { api } from \"../api.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\nimport { showToast } from \"../components/toast.js\";\n\nexport async function loadMigrations(sectionContainer) {\n renderLoading(sectionContainer, \"Loading migrations…\");\n\n try {\n const data = await api(\"/api/migrations\");\n const migrations = data.migrations || [];\n\n const html = `\n <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px\">\n <h3>Migrations</h3>\n <button id=\"runAllPending\" class=\"btn btn--primary btn--sm\">\n <i class=\"ri-play-line\"></i> Run all pending\n </button>\n </div>\n <div id=\"migrationList\">\n ${migrations.map((m) => `\n <div class=\"migration-item\">\n <span class=\"badge badge--${m.status === 'applied' ? 'ok' : 'pending'}\">${m.status}</span>\n <code style=\"font-size:.8rem\">${m.name}</code>\n <span class=\"text-muted\">${m.description || \"\"}</span>\n ${m.status === 'pending'\n ? `<button class=\"btn btn--sm run-migration\" data-name=\"${m.name}\">Run</button>`\n : \"\"\n }\n </div>\n `).join(\"\")}\n </div>\n <pre id=\"migrationOutput\" class=\"code-block hidden\" style=\"white-space:pre-wrap;font-size:.75rem;color:#888\"></pre>\n `;\n\n renderSuccess(sectionContainer, html);\n\n // Bind run buttons\n sectionContainer.querySelectorAll(\".run-migration\").forEach((btn) => {\n btn.addEventListener(\"click\", () => runMigration(btn.dataset.name));\n });\n\n document.getElementById(\"runAllPending\")?.addEventListener(\"click\", runAllPending);\n } catch (e) {\n renderError(sectionContainer, `Failed to load migrations: ${e.message}`);\n }\n}\n\nasync function runMigration(name) {\n if (!confirm(`Run migration '${name}'? This executes SQL directly.`)) return;\n\n try {\n const data = await api(`/api/migrations/${name}/run`, { method: \"POST\" });\n const output = document.getElementById(\"migrationOutput\");\n if (output) {\n output.classList.remove(\"hidden\");\n output.textContent = data.output || \"Migration applied\";\n }\n if (data.ok) {\n showToast(`Migration '${name}' applied`, \"ok\");\n } else {\n showToast(`Migration '${name}' failed: ${data.error}`, \"error\");\n }\n } catch (e) {\n showToast(`Migration failed: ${e.message}`, \"error\");\n }\n}\n\nasync function runAllPending() {\n try {\n const data = await api(\"/api/migrations/pending/run\", { method: \"POST\" });\n const output = document.getElementById(\"migrationOutput\");\n if (output) {\n output.classList.remove(\"hidden\");\n output.textContent = data.output || \"All pending migrations applied\";\n }\n showToast(\"All pending migrations applied\", \"ok\");\n } catch (e) {\n showToast(`Failed: ${e.message}`, \"error\");\n }\n}\n",
|
|
27
|
+
"/**\n * Users section — list users and manage roles.\n */\n\nimport { api } from \"../api.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\nimport { showToast } from \"../components/toast.js\";\n\nexport async function loadUsers(sectionContainer) {\n renderLoading(sectionContainer, \"Loading users…\");\n\n try {\n const data = await api(\"/api/users\");\n const users = data.users || [];\n\n if (!users.length) {\n renderSuccess(sectionContainer, '<div class=\"text-muted\">No users found.</div>');\n return;\n }\n\n const html = `\n <div class=\"table-wrapper\">\n <table class=\"data-table\">\n <thead><tr><th>ID</th><th>Email</th><th>Role</th><th>Created</th><th>Roles</th></tr></thead>\n <tbody>\n ${users.map((u) => `\n <tr>\n <td style=\"font-family:var(--mono);font-size:.8rem\">${u.id?.slice?.(0, 8) || \"—\" }…</td>\n <td>${u.email || \"—\"}</td>\n <td>${u.role || \"user\"}</td>\n <td class=\"text-muted\">${u.created_at ? new Date(u.created_at).toLocaleDateString() : \"—\"}</td>\n <td><button class=\"btn btn--sm manage-roles-btn\" data-user-id=\"${u.id}\" style=\"font-size:.7rem\">Manage roles</button></td>\n </tr>\n `).join(\"\")}\n </tbody>\n </table>\n </div>\n <div id=\"userRolePanel\" class=\"hidden\" style=\"margin-top:12px\"></div>\n `;\n\n renderSuccess(sectionContainer, html);\n\n sectionContainer.querySelectorAll(\".manage-roles-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => showRolePanel(btn.dataset.userId));\n });\n } catch (e) {\n renderError(sectionContainer, `Failed to load users: ${e.message}`);\n }\n}\n\nasync function showRolePanel(userId) {\n const panel = document.getElementById(\"userRolePanel\");\n panel.classList.remove(\"hidden\");\n panel.innerHTML = 'Loading…';\n\n try {\n const data = await api(`/api/users/${userId}/roles`);\n const roles = data.roles || [];\n const availableRoles = [\"admin\", \"user\", \"guest\"];\n\n panel.innerHTML = `\n <div style=\"padding:8px 12px;background:#111;border-radius:6px\">\n <div style=\"font-size:.8rem;color:#888;margin-bottom:8px\">User: ${userId.slice(0, 8)}…</div>\n <div style=\"display:flex;gap:6px;flex-wrap:wrap\">\n ${availableRoles.map((r) => `\n <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)}\">\n ${r} ${roles.includes(r) ? \"✓\" : \"\"}\n </button>\n `).join(\"\")}\n </div>\n </div>\n `;\n\n panel.querySelectorAll(\".role-toggle-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", async () => {\n const role = btn.dataset.role;\n const uid = btn.dataset.userId;\n const isActive = btn.dataset.active === \"true\";\n\n try {\n if (isActive) {\n await api(`/api/users/${uid}/roles/${role}`, { method: \"DELETE\" });\n } else {\n await api(`/api/users/${uid}/roles`, { method: \"POST\", body: JSON.stringify({ role }) });\n }\n showToast(`Role '${role}' ${isActive ? \"removed\" : \"assigned\"}`, \"ok\");\n showRolePanel(uid);\n } catch (e) {\n showToast(e.message, \"error\");\n }\n });\n });\n } catch (e) {\n panel.innerHTML = `<div class=\"text-muted\">Failed to load roles: ${e.message}</div>`;\n }\n}\n",
|
|
28
|
+
"/**\n * Roles section — list roles and role assignments.\n */\n\nimport { api } from \"../api.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\nimport { showToast } from \"../components/toast.js\";\n\nexport async function loadRoles(sectionContainer) {\n renderLoading(sectionContainer, \"Loading roles…\");\n\n try {\n const data = await api(\"/api/roles\");\n const roles = data.roles || [];\n\n if (!roles.length) {\n renderSuccess(sectionContainer, `\n <div style=\"display:flex;justify-content:space-between;align-items:center\">\n <div class=\"text-muted\">No roles defined.</div>\n <button id=\"addRoleBtn\" class=\"btn btn--primary btn--sm\"><i class=\"ri-add-line\"></i> Add role</button>\n </div>\n `);\n document.getElementById(\"addRoleBtn\")?.addEventListener(\"click\", createRoleInline);\n return;\n }\n\n const html = `\n <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px\">\n <div style=\"display:flex;gap:6px;flex-wrap:wrap\">\n ${roles.map((r) => `<span class=\"role-chip\">${r.name}</span>`).join(\"\")}\n </div>\n <button id=\"addRoleBtn\" class=\"btn btn--primary btn--sm\"><i class=\"ri-add-line\"></i> Add role</button>\n </div>\n `;\n\n renderSuccess(sectionContainer, html);\n document.getElementById(\"addRoleBtn\")?.addEventListener(\"click\", createRoleInline);\n } catch (e) {\n renderError(sectionContainer, `Failed to load roles: ${e.message}`);\n }\n}\n\nasync function createRoleInline() {\n const name = prompt(\"Role name (e.g. editor):\");\n if (!name) return;\n\n try {\n const data = await api(\"/api/roles\", {\n method: \"POST\",\n body: JSON.stringify({ name, description: \"Custom role\" }),\n });\n if (data.success) {\n showToast(`Role '${name}' created`, \"ok\");\n loadRoles(document.querySelector('[data-section=\"roles\"]') || document.getElementById(\"roles\"));\n } else {\n showToast(data.error || \"Failed\", \"error\");\n }\n } catch (e) {\n showToast(e.message, \"error\");\n }\n}\n",
|
|
29
|
+
"/**\n * API Keys section — generate and validate keys.\n */\n\nimport { api } from \"../api.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\nimport { showToast } from \"../components/toast.js\";\n\nexport async function loadApiKeys(sectionContainer) {\n renderLoading(sectionContainer, \"Loading API keys…\");\n\n try {\n const data = await api(\"/api/keys\");\n const keys = data.keys || [];\n\n const html = `\n <h3>Generate API Key</h3>\n <p class=\"text-muted\" style=\"margin-bottom:12px\">Create a key for programmatic access. The raw key is shown <strong>only once</strong>.</p>\n <div style=\"display:flex;gap:8px;align-items:end;flex-wrap:wrap;margin-bottom:16px\">\n <input id=\"keyName\" placeholder=\"Key name\" style=\"background:#000;border:1px solid #333;color:#fff;padding:8px 12px;border-radius:4px;font-size:.85rem\" />\n <button id=\"generateKeyBtn\" class=\"btn btn--primary btn--sm\">Generate</button>\n </div>\n <pre id=\"keyResult\" class=\"code-block hidden\" style=\"white-space:pre-wrap;font-size:.8rem;color:#22c55e\"></pre>\n <h3 style=\"margin-top:16px\">Existing Keys</h3>\n ${keys.length\n ? `<div style=\"display:flex;flex-direction:column;gap:6px\">\n ${keys.map((k) => `\n <div style=\"display:flex;gap:8px;align-items:center;padding:8px 12px;background:#111;border-radius:6px\">\n <code style=\"flex:1;font-size:.8rem\">authly_...${k.key_hash?.slice?.(-6) || \"—\"}</code>\n <span class=\"text-muted\" style=\"font-size:.75rem\">${k.name || \"\"}</span>\n <span class=\"text-muted\" style=\"font-size:.75rem\">${k.scopes?.join?.(\", \") || \"\"}</span>\n <button class=\"btn btn--sm delete-key-btn\" data-key-id=\"${k.id}\">Delete</button>\n </div>\n `).join(\"\")}\n </div>`\n : '<div class=\"text-muted\">No API keys yet.</div>'\n }\n `;\n\n renderSuccess(sectionContainer, html);\n\n document.getElementById(\"generateKeyBtn\")?.addEventListener(\"click\", generateKey);\n sectionContainer.querySelectorAll(\".delete-key-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => deleteKey(btn.dataset.keyId));\n });\n } catch (e) {\n renderError(sectionContainer, `Failed to load API keys: ${e.message}`);\n }\n}\n\nasync function generateKey() {\n const name = document.getElementById(\"keyName\").value.trim();\n if (!name) return;\n\n const result = document.getElementById(\"keyResult\");\n result.classList.remove(\"hidden\");\n result.textContent = \"Generating…\";\n\n try {\n const data = await api(\"/api/keys\", {\n method: \"POST\",\n body: JSON.stringify({ name }),\n });\n if (data.success && data.key) {\n result.textContent = `Key (copy now):\\n${data.key}`;\n showToast(\"API key generated — copy it now!\", \"ok\");\n } else {\n result.textContent = `Error: ${data.error || \"Unknown error\"}`;\n }\n } catch (e) {\n result.textContent = e.message;\n }\n}\n\nasync function deleteKey(id) {\n if (!confirm(\"Delete this API key?\")) return;\n\n try {\n await api(`/api/keys/${id}`, { method: \"DELETE\" });\n showToast(\"API key deleted\", \"ok\");\n loadApiKeys(document.querySelector('[data-section=\"api-keys\"]') || document.getElementById(\"api-keys\"));\n } catch (e) {\n showToast(e.message, \"error\");\n }\n}\n",
|
|
30
|
+
"/**\n * Audit section — run configuration health checks.\n */\n\nimport { api } from \"../api.js\";\nimport { renderLoading, renderError, renderSuccess } from \"../components/states.js\";\n\nexport async function loadAudit(sectionContainer) {\n renderLoading(sectionContainer, \"Running audit…\");\n\n try {\n const data = await api(\"/api/audit\");\n const issues = data.issues || [];\n\n const html = `\n <div style=\"margin-bottom:12px\">\n <strong style=\"font-size:.9rem\">${issues.every(i => i.level !== \"error\") ? \"✔ Audit passed\" : `✘ ${issues.filter(i => i.level === \"error\").length} issue(s)`}</strong>\n </div>\n <div style=\"display:flex;flex-direction:column;gap:6px\">\n ${issues.map(i => `\n <div style=\"display:flex;gap:8px;align-items:center;padding:8px 12px;background:#111;border-radius:6px\">\n <span>${i.level === \"error\"\n ? '<i class=\"ri-error-warning-line\" style=\"color:#f87171\"></i>'\n : i.level === \"warn\"\n ? '<i class=\"ri-alert-line\" style=\"color:#f59e0b\"></i>'\n : '<i class=\"ri-check-line\" style=\"color:#22c55e\"></i>'\n }</span>\n <span style=\"font-size:.85rem\">${i.message || i.check || \"\"}</span>\n </div>\n `).join(\"\")}\n </div>\n `;\n\n renderSuccess(sectionContainer, html);\n } catch (e) {\n renderError(sectionContainer, `Audit failed: ${e.message}`);\n }\n}\n",
|
|
31
|
+
"/**\n * MCP section — info and connection instructions.\n */\n\nimport { renderSuccess } from \"../components/states.js\";\n\nexport function loadMCP(sectionContainer) {\n renderSuccess(sectionContainer, `\n <div>\n <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:8px\">\n <i class=\"ri-robot-line\" style=\"font-size:1.2rem;color:#fff\"></i>\n <h3 style=\"margin:0\">MCP Server</h3>\n <span class=\"badge\">BETA</span>\n </div>\n <p class=\"text-muted\" style=\"margin-bottom:12px\">Connect an MCP client (Claude Desktop, Cursor) to manage Supabase through tools.</p>\n <pre class=\"code-block\"><code>Connect to:\n http://localhost:${location.port || 1284}/mcp\n\nTools available:\n execute_sql, list_tables, describe_table\n list_auth_users, list_roles, assign_role_to_user\n revoke_role_from_user, get_user_roles\n list_migrations, get_migration_sql, run_migration\n connection_info</code></pre>\n <p style=\"font-size:.8rem;color:#666;margin-top:12px\">\n <i class=\"ri-error-warning-line\"></i> MCP tools are experimental.\n </p>\n </div>\n `);\n}\n",
|
|
32
|
+
"/**\n * Shared state renderers for all sections.\n * Every section uses exactly these 3 states.\n */\n\nexport function renderLoading(container, message = \"Loading...\") {\n container.innerHTML = `<div class=\"state state--loading\"><i class=\"ri-loader-4-line spinner\"></i><span>${message}</span></div>`;\n}\n\nexport function renderError(container, message) {\n container.innerHTML = `<div class=\"state state--error\"><i class=\"ri-error-warning-line\"></i><span>${message}</span></div>`;\n}\n\nexport function renderSuccess(container, content) {\n container.innerHTML = `<div class=\"state state--success\">${content}</div>`;\n}\n",
|
|
33
|
+
"/**\n * Reusable wizard/sidebar component.\n * Opens a panel with tabbed content for provider setup, etc.\n */\n\nexport function openWizard(options = {}) {\n const { title = \"Setup\", tabs = [], onClose } = options;\n const existing = document.getElementById(\"wizard\");\n if (existing) existing.remove();\n\n const wizard = document.createElement(\"aside\");\n wizard.id = \"wizard\";\n wizard.className = \"wizard\";\n wizard.innerHTML = `\n <div class=\"wizard__header\">\n <h3>${title}</h3>\n <button id=\"wizardClose\" aria-label=\"Close\"><i class=\"ri-close-line\"></i></button>\n </div>\n <nav class=\"wizard__tabs\" id=\"wizardTabs\">\n ${tabs.map((t, i) => `<button class=\"wizard__tab${i === 0 ? \" active\" : \"\"}\" data-tab=\"${i}\">${t.label}</button>`).join(\"\")}\n </nav>\n <div class=\"wizard__body\" id=\"wizardBody\">\n ${tabs[0]?.content ?? \"\"}\n </div>\n `;\n\n document.querySelector(\".main\")?.appendChild(wizard);\n\n // Tab switching\n wizard.querySelectorAll(\".wizard__tab\").forEach((tab) => {\n tab.addEventListener(\"click\", () => {\n wizard.querySelectorAll(\".wizard__tab\").forEach((t) => t.classList.remove(\"active\"));\n tab.classList.add(\"active\");\n const idx = tab.dataset.tab;\n // Replace body content with the tab's content\n const content = tabs[parseInt(idx)]?.content ?? \"\";\n document.getElementById(\"wizardBody\").innerHTML = content;\n // Call any attached handlers\n if (tabs[parseInt(idx)]?.onShow) tabs[parseInt(idx)].onShow();\n });\n });\n\n // Close handler\n document.getElementById(\"wizardClose\")?.addEventListener(\"click\", () => {\n wizard.remove();\n onClose?.();\n });\n}\n\nexport function closeWizard() {\n document.getElementById(\"wizard\")?.remove();\n}\n"
|
|
34
|
+
],
|
|
35
|
+
"mappings": "AAAA;AAAA,;AAAA,,;AAAA,,,;AAAA,,,,;AAAA,,,,,;AAAA,,,,,,;AAAA,,,,,,,;AAAA,,,,,,,,;AAAA,,,,,,,,,;AAAA,,,,,,,,,,;AAAA,,,,,,,,,,,;AAAA,,,,,,,,,,,,;AAAA,,,,,,,,,,,,,",
|
|
36
|
+
"file": "app.js",
|
|
37
|
+
"sourceRoot": ""
|
|
38
|
+
}
|
|
@@ -125,43 +125,15 @@
|
|
|
125
125
|
|
|
126
126
|
<!-- ═══ INIT ═══ -->
|
|
127
127
|
<section class="section" id="init">
|
|
128
|
-
<div class="doc-panel" id="initDocs">
|
|
129
|
-
<h4><i class="ri-book-line"></i> Auto-detection <button class="toggle-docs" data-target="initDocsBody">toggle</button></h4>
|
|
130
|
-
<div class="doc-panel-body expanded" id="initDocsBody">
|
|
131
|
-
<p>Authly scans your project files for Supabase credentials — no manual configuration needed.</p>
|
|
132
|
-
<ul>
|
|
133
|
-
<li>Checks <code>.env.local</code>, <code>.env.development.local</code>, <code>.env.development</code>, <code>.env</code></li>
|
|
134
|
-
<li>Also reads <code>supabase/config.toml</code> if present</li>
|
|
135
|
-
<li>If found — connects automatically</li>
|
|
136
|
-
<li>If not found — fill in manually below</li>
|
|
137
|
-
</ul>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
<!-- Auto-detected status -->
|
|
141
|
-
<div class="card" id="autoDetectCard">
|
|
142
|
-
<div class="card__icon"><i class="ri-search-eye-line"></i></div>
|
|
143
|
-
<h2>Project scan</h2>
|
|
144
|
-
<div id="scanStatus" style="font-size:.85rem;color:#888">Scanning…</div>
|
|
145
|
-
<div id="scanDetail" class="hidden" style="margin-top:12px;font-size:.8rem"></div>
|
|
146
|
-
<div id="scanAction" class="hidden" style="margin-top:12px"></div>
|
|
147
|
-
</div>
|
|
148
|
-
<!-- Manual fallback -->
|
|
149
128
|
<div class="card">
|
|
150
129
|
<div class="card__icon"><i class="ri-flashlight-line"></i></div>
|
|
151
|
-
<h2>
|
|
152
|
-
<p style="margin-
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
<label>Supabase Anon Key</label>
|
|
157
|
-
<input type="password" id="initAnon" placeholder="eyJh…" />
|
|
158
|
-
<label>Service Role Key (admin)</label>
|
|
159
|
-
<input type="password" id="initService" placeholder="eyJh…" />
|
|
160
|
-
<div id="initResult" class="result hidden"></div>
|
|
161
|
-
<button type="submit" class="btn btn--primary" id="initBtn">
|
|
162
|
-
<i class="ri-plug-line"></i> Connect & Configure
|
|
130
|
+
<h2>Authly is running</h2>
|
|
131
|
+
<p class="text-muted" style="margin-top:8px">Open the <strong>Integration</strong> section to connect Supabase and configure providers.</p>
|
|
132
|
+
<div style="margin-top:12px">
|
|
133
|
+
<button class="btn btn--primary btn--sm" onclick="document.querySelector('[data-section=integration]')?.click()">
|
|
134
|
+
<i class="ri-database-2-line"></i> Go to Integration
|
|
163
135
|
</button>
|
|
164
|
-
</
|
|
136
|
+
</div>
|
|
165
137
|
</div>
|
|
166
138
|
</section>
|
|
167
139
|
|
|
@@ -170,25 +142,16 @@
|
|
|
170
142
|
<div class="doc-panel" id="intDocs">
|
|
171
143
|
<h4><i class="ri-book-line"></i> Supabase integration <button class="toggle-docs" data-target="intDocsBody">toggle</button></h4>
|
|
172
144
|
<div class="doc-panel-body expanded" id="intDocsBody">
|
|
173
|
-
<p>Authly connects directly to your Supabase database to manage users, roles, sessions, and migrations.</p>
|
|
145
|
+
<p>Authly connects directly to your Supabase database to manage users, roles, sessions, and migrations. Credentials are found automatically by scanning your project's .env files.</p>
|
|
174
146
|
<ul>
|
|
175
|
-
<li>
|
|
176
|
-
<li>
|
|
177
|
-
<li>
|
|
178
|
-
<li>Magic link tokens in <code>authly_magic_links</code></li>
|
|
147
|
+
<li>Scans <code>.env.local</code>, <code>supabase/config.toml</code> and other env files</li>
|
|
148
|
+
<li>If scanning fails — connect via Supabase Platform OAuth</li>
|
|
149
|
+
<li>Users are stored in <code>authly_users</code> table</li>
|
|
179
150
|
</ul>
|
|
180
|
-
<p style="margin-top:8px">Connection is automatic if Supabase credentials exist in your env files.</p>
|
|
181
151
|
</div>
|
|
182
152
|
</div>
|
|
183
|
-
<div class="card">
|
|
184
|
-
|
|
185
|
-
<h2>Supabase connection</h2>
|
|
186
|
-
<div id="integrationStatus" style="font-size:.85rem;color:#888">Checking…</div>
|
|
187
|
-
<div id="integrationDetail" class="hidden" style="margin-top:12px"></div>
|
|
188
|
-
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">
|
|
189
|
-
<button class="btn btn--primary btn--sm" id="reconnectBtn"><i class="ri-refresh-line"></i> Re-scan & reconnect</button>
|
|
190
|
-
<a href="https://authly.rblez.com/api/auth/supabase/authorize" target="_blank" class="btn btn--sm" style="background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff;text-decoration:none" id="connectSbPlatformBtn"><i class="ri-plug-line"></i> Connect via OAuth</a>
|
|
191
|
-
</div>
|
|
153
|
+
<div class="card" id="integrationCard">
|
|
154
|
+
<!-- Dynamically rendered by sections/supabase.js -->
|
|
192
155
|
</div>
|
|
193
156
|
</section>
|
|
194
157
|
|
|
@@ -728,6 +728,67 @@ code {
|
|
|
728
728
|
color: var(--text-dim);
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
+
/* ── State renderers (3 states per section) ────────── */
|
|
732
|
+
.state {
|
|
733
|
+
padding: 24px 20px;
|
|
734
|
+
border-radius: var(--radius);
|
|
735
|
+
display: flex;
|
|
736
|
+
flex-direction: column;
|
|
737
|
+
align-items: center;
|
|
738
|
+
gap: 8px;
|
|
739
|
+
text-align: center;
|
|
740
|
+
}
|
|
741
|
+
.state--loading { color: var(--text-dim); }
|
|
742
|
+
.state--error { background: #1c0a0a; border: 1px solid #7f1d1d; color: #f87171; }
|
|
743
|
+
.state--success { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
|
|
744
|
+
.state i { font-size: 1.3rem; }
|
|
745
|
+
|
|
746
|
+
/* ── Spinner ───────────────────────────────────────── */
|
|
747
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
748
|
+
.spinner { animation: spin 1s linear infinite; display: inline-block; }
|
|
749
|
+
|
|
750
|
+
/* ── Wizard sidebar ──────────────────────────────── */
|
|
751
|
+
.wizard {
|
|
752
|
+
position: fixed; top: 0; right: 0; width: 420px; max-width: 100vw;
|
|
753
|
+
height: 100vh; background: #0a0a0a; border-left: 1px solid var(--border);
|
|
754
|
+
z-index: 200; display: flex; flex-direction: column;
|
|
755
|
+
animation: slideLeft 0.2s ease;
|
|
756
|
+
}
|
|
757
|
+
@keyframes slideLeft { from { transform: translateX(100%); } to { transform: translateX(0); } }
|
|
758
|
+
|
|
759
|
+
.wizard__header {
|
|
760
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
761
|
+
padding: 16px 20px; border-bottom: 1px solid var(--border);
|
|
762
|
+
}
|
|
763
|
+
.wizard__header h3 { font-size: 0.95rem; margin: 0; }
|
|
764
|
+
.wizard__header button {
|
|
765
|
+
background: none; border: none; color: var(--text-dim); cursor: pointer;
|
|
766
|
+
font-size: 1.2rem; padding: 4px;
|
|
767
|
+
}
|
|
768
|
+
.wizard__header button:hover { color: var(--text); }
|
|
769
|
+
|
|
770
|
+
.wizard__tabs {
|
|
771
|
+
display: flex; border-bottom: 1px solid var(--border);
|
|
772
|
+
}
|
|
773
|
+
.wizard__tab {
|
|
774
|
+
flex: 1; padding: 10px 8px; background: none; border: none;
|
|
775
|
+
color: var(--text-dim); font-size: 0.8rem; cursor: pointer;
|
|
776
|
+
border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s;
|
|
777
|
+
font-family: var(--font);
|
|
778
|
+
}
|
|
779
|
+
.wizard__tab.active { color: var(--text); border-bottom-color: var(--text); }
|
|
780
|
+
.wizard__tab:hover { color: var(--text); }
|
|
781
|
+
|
|
782
|
+
.wizard__body { flex: 1; padding: 20px; overflow-y: auto; }
|
|
783
|
+
.wizard__body h4 { font-size: 0.9rem; margin-bottom: 8px; }
|
|
784
|
+
|
|
785
|
+
/* ── Badge variants ────────────────────────────────── */
|
|
786
|
+
.badge--ok { color: #22c55e; background: #052e16; border-color: #166534; }
|
|
787
|
+
.badge--pending { color: var(--text-dim); background: var(--bg); border-color: var(--border); }
|
|
788
|
+
|
|
789
|
+
/* ── Text utilities ────────────────────────────────── */
|
|
790
|
+
.text-muted { color: var(--text-dim); font-size: 0.8rem; }
|
|
791
|
+
|
|
731
792
|
/* ── Mobile ──────────────────────────────────────────── */
|
|
732
793
|
@media (max-width: 768px) {
|
|
733
794
|
.sidebar {
|
|
@@ -749,4 +810,6 @@ code {
|
|
|
749
810
|
.grid-3 {
|
|
750
811
|
grid-template-columns: 1fr;
|
|
751
812
|
}
|
|
813
|
+
|
|
814
|
+
.wizard { width: 100vw; }
|
|
752
815
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rblez/authly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local auth dashboard for Next.js + Supabase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/authly.js serve",
|
|
11
11
|
"dev": "node bin/authly.js serve",
|
|
12
|
+
"build:dashboard": "node scripts/build.js",
|
|
13
|
+
"unpack": "node scripts/unpack.js",
|
|
12
14
|
"lint": "prettier --check src/",
|
|
13
15
|
"format": "prettier --write src/"
|
|
14
16
|
},
|