diff --git a/README.md b/README.md index 916ac76..1ee9cc8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Docker Architectures Caddy - Version + Version

Why Site Gateway · @@ -44,9 +44,10 @@ It's intentionally narrower than a general-purpose proxy manager. You describe * - **Access Lists** — reusable login/network policies combining accounts, groups, and IP/CIDR rules across any host. - **Two-factor authentication** — TOTP-based MFA for administrator and user accounts, with recovery codes, plus an administrator-side override to disable a locked-out user's 2FA when they've lost their authenticator and used up their recovery codes. - **Users, groups, and roles** — Administrator and Standard User roles, with account lifecycle controls. +- **API access tokens** — issue scoped (full-access or read-only), optionally expiring bearer tokens for scripts and integrations, revocable at any time. - **Backups** — configuration or complete `.sgbackup` archives, downloadable, importable, schedulable, and optionally AES-256-GCM encrypted. - **Certificates page** — issuer, expiration, days remaining, and renewal health for every managed and uploaded certificate. -- **Performance and logs** — request throughput, response times, and rotating access/activity logs per host. +- **Performance and logs** — per-domain request throughput, response times, and rotating access/activity logs, including a System page with environment/integration status, gateway sync, scheduled jobs, and storage usage. - **SQLite-backed persistence** — no external database container; everything lives under one `/data` volume. Hosted uploads remain static-only (HTML, CSS, JS, images, fonts, downloads). Dynamic applications are connected as Proxy Hosts instead — Site Gateway does not execute uploaded PHP, Node, Python, or database code. diff --git a/ROADMAP.md b/ROADMAP.md index b6786ae..4d4b515 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -162,3 +162,5 @@ Roughly in priority order: `v0.16.17` fixes the Backup type helper text showing both the Complete and Configuration-only explanations stacked on top of each other on page load or refresh, instead of just the one matching the currently selected option. Root cause: the help text only ever updated on the select's `change` event -- but `renderBackups()` sets the select's value from saved settings on every render without firing a `change` event, so the static placeholder text (which briefly held both sentences as a v0.16.16 authoring mistake) never got replaced until you manually touched the dropdown. Factored the text-selection logic into its own function and call it both on `change` and every time `renderBackups()` runs, so it always matches the select's actual current value. `v0.16.18` reworks the API Access tab to match the Users and Groups tabs' layout instead of the old plain data-row list: tokens are now shown as tiles in the same card grid Hosted Sites/Users/Groups use, and a stat bar above them breaks down Active/Revoked and Full access/Read-only counts at a glance. No behavior changed -- Revoke still works the same way it always has (a one-way action; there is no re-enable, since a revoked token's secret is treated as compromised). An earlier idea of adding an enable/disable toggle was dropped once it became clear that would require adding real token-reactivation support on the backend, a deliberate security-posture change rather than a layout fix. + +`v0.16.19` finishes the API Access tab's alignment with Users and Groups: the "Create token" button now lives in the shared top-right header button used by every other create action instead of its own row inside the panel, and the panel-heading text ("Programmatic access / API access tokens / Issue bearer tokens...") has been removed the same way it was for Groups in v0.16.15, since the tab button's own label already says what the section is -- the stat bar is now the first thing in the panel. Also walked the in-app Documentation view and brought it current with everything shipped since it was last substantively updated: added a full API Access section (creating a token, scope, expiry, the one-time reveal, revoking, and automatic revocation when an issuing administrator's password changes or account is disabled), corrected the Performance section's per-route table description to drop the removed per-row error-count badge and instead document the pinned column headers and the "Not configured" chip added in v0.16.16, and added an API Access entry to the documentation sidebar's contents list. diff --git a/package.json b/package.json index 594532f..e717a17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "site-gateway", - "version": "0.16.18", + "version": "0.16.19", "private": true, "description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.", "type": "module", diff --git a/src/public/app.js b/src/public/app.js index b333ce0..2c3abb9 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -501,8 +501,8 @@ function render() { if (state.view === "administration") { const adminTab = state.adminTab || "users"; document.querySelectorAll("[data-admin-tab]").forEach(item => item.classList.toggle("tab-active", item.dataset.adminTab === adminTab)); document.querySelectorAll("[data-admin-panel]").forEach(panel => panel.classList.toggle("hidden", panel.dataset.adminPanel !== adminTab)); } $("#streaming-view").classList.toggle("hidden", state.view !== "streaming"); $("#redirects-view").classList.toggle("hidden", state.view !== "redirects"); $("#access-view").classList.toggle("hidden", state.view !== "access"); $("#documentation-view").classList.toggle("hidden", state.view !== "documentation"); const activeAdminTab = state.view === "administration" ? document.querySelector("[data-admin-tab].tab-active")?.dataset.adminTab : null; - const adminUsersActive = activeAdminTab === "users", adminGroupsActive = activeAdminTab === "groups"; - $("#open-create").classList.toggle("hidden", !(management || adminUsersActive || adminGroupsActive || ["streaming","redirects","access"].includes(state.view)) || !canManage()); $("#check-health").classList.toggle("hidden", state.view !== "certificates"); $("#refresh-logs").classList.toggle("hidden", state.view !== "logs"); + const adminUsersActive = activeAdminTab === "users", adminGroupsActive = activeAdminTab === "groups", adminApiActive = activeAdminTab === "api"; + $("#open-create").classList.toggle("hidden", !(management || adminUsersActive || adminGroupsActive || adminApiActive || ["streaming","redirects","access"].includes(state.view)) || !canManage()); $("#check-health").classList.toggle("hidden", state.view !== "certificates"); $("#refresh-logs").classList.toggle("hidden", state.view !== "logs"); if (overview) { $("#page-title").textContent = "Dashboard"; $("#page-subtitle").textContent = "Health, activity, and system status at a glance."; @@ -512,7 +512,7 @@ function render() { if (!management) { const headings = { certificates:["Certificates","Expiration, issuer, and certificate-detection status for automatic HTTPS."], logs:["Access Logs & Gateway Events","Recent requests, upstream responses, and gateway health events served through Caddy."], performance:["Performance","Live and historical request throughput across your gateway."], administration:["Administration","Users, gateway defaults, backups, and updates."], streaming:["Streaming hosts","Forward raw TCP/UDP traffic on a specific port straight to another host and port."], redirects:["Redirect hosts","Send domains to a new destination with clear, predictable rules."], access:["Access Lists","Create reusable network and login protection for your hosts."], documentation:["Documentation","Plain-language guidance and real-world Site Gateway examples."], account:["My Account","Manage your profile, password, and two-factor authentication."] }; const heading = headings[state.view] || ["Site Gateway",""]; $("#page-title").textContent = heading[0]; $("#page-subtitle").textContent = heading[1]; - $("#open-create").textContent = state.view === "administration" ? (adminGroupsActive ? "+ Create group" : "+ Create user") : state.view === "streaming" ? "+ New streaming host" : state.view === "redirects" ? "+ New redirect host" : state.view === "access" ? "+ New Access List" : $("#open-create").textContent; + $("#open-create").textContent = state.view === "administration" ? (adminGroupsActive ? "+ Create group" : adminApiActive ? "+ Create token" : "+ Create user") : state.view === "streaming" ? "+ New streaming host" : state.view === "redirects" ? "+ New redirect host" : state.view === "access" ? "+ New Access List" : $("#open-create").textContent; if (state.view === "streaming") $("#stream-empty").classList.toggle("hidden", !state.loaded || state.streams.length > 0); if (state.view === "streaming") { const items = state.streams; const running = items.filter(item => item.status === "running").length, disabled = items.filter(item => item.status === "disabled").length, errors = items.filter(item => item.status === "error").length; $("#running-count").textContent = running; $("#disabled-count").textContent = disabled; $("#error-count").textContent = errors; $("#running-label").textContent = running ? "Running" : "None running"; $("#disabled-label").textContent = disabled ? "Disabled" : "None disabled"; $("#error-label").textContent = errors ? "Needs attention" : "No issues"; $("#running-dot").className = `status-dot ${running ? "running" : "inactive"}`; $("#disabled-dot").className = `status-dot ${disabled ? "disabled" : "inactive"}`; $("#error-dot").className = `status-dot ${errors ? "error" : "inactive"}`; $(".port-note").classList.add("hidden"); } if (state.view === "redirects") $("#redirect-empty .create-trigger").textContent = "Create a redirect host"; @@ -657,6 +657,7 @@ $("#event-category").addEventListener("change", renderLogs); // --- "Create" dialog: opens the right create form/dialog for the current view -------------- function openCreate() { if (state.view === "administration" && state.adminTab === "groups") { openNewGroupEditor(); return; } + if (state.view === "administration" && state.adminTab === "api") { openCreateApiTokenDialog(); return; } if (state.view === "administration") { $("#user-form").reset(); $("#user-error").textContent = ""; return $("#user-dialog").showModal(); } if (state.view === "streaming") { $("#stream-form").reset(); delete $("#stream-form").dataset.editing; $("#stream-title").textContent = "Create a streaming host"; $("#stream-form .button.primary").textContent = "Create streaming host"; $("#stream-error").textContent = ""; return $("#stream-dialog").showModal(); } if (state.view === "redirects") { $("#redirect-form").reset(); delete $("#redirect-form").dataset.editing; $("#redirect-error").textContent = ""; return $("#redirect-dialog").showModal(); } diff --git a/src/public/features.js b/src/public/features.js index d946bb6..85bc07a 100644 --- a/src/public/features.js +++ b/src/public/features.js @@ -378,7 +378,7 @@ function renderApiTokensPanel() { if (!panel) { panel = document.createElement("section"); panel.dataset.adminPanel = "api"; panel.className = "settings-panel hidden"; users.parentElement.append(panel); } if (panel.dataset.ready) return; panel.dataset.ready = "1"; - panel.innerHTML = '

Programmatic access

API access tokens

Issue bearer tokens for scripts and integrations. A token acts as the administrator who issued it, and is shown in full only once. Changing that administrator’s password, or disabling their account, revokes every token they issued.

Open this tab to load API tokens.

'; + panel.innerHTML = '

Open this tab to load API tokens.

'; tab.addEventListener("click", async () => { document.querySelectorAll("[data-admin-tab]").forEach(item => item.classList.toggle("tab-active", item === tab)); document.querySelectorAll("[data-admin-panel]").forEach(item => item.classList.toggle("hidden", item !== panel)); @@ -395,8 +395,10 @@ function showIssuedApiToken(result) { }); dialog.showModal(); } -document.addEventListener("click", async event => { - if (!event.target.closest("#create-api-token")) return; +// Called from the shared global "+ Create" header button (see openCreate() in app.js) when the API +// Access tab is active -- there is no dedicated "Create token" button in the panel itself anymore, +// matching how Groups and Users route their create actions through the same shared button. +async function openCreateApiTokenDialog() { const dialog = featureDialog("create-api-token-dialog"); dialog.innerHTML = '

API access

Create an API token

Re-enter your administrator credentials to confirm. The token inherits your role.

'; dialog.showModal(); @@ -409,7 +411,7 @@ document.addEventListener("click", async event => { await loadApiTokens(); showIssuedApiToken(result); } catch (error) { toast(error.message, "error"); } -}); +} document.addEventListener("click", async event => { const button = event.target.closest('[data-token-action="revoke"]'); if (!button) return; const row = button.closest("[data-token-id]"); if (!row) return; diff --git a/src/public/index.html b/src/public/index.html index f1f957c..e4e3d59 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -8,7 +8,7 @@ Site Gateway - + - +