diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9a292..7cf03d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,15 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`. ## [Unreleased] -## [1.1.12] - 2026-07-17 +## [1.1.13] - 2026-07-17 + +### Added +- Four new plain-language docs aimed at less technical readers, replacing the system-design-level Architecture/Installation docs as the target of most card help links: **Hosts & HTTPS**, **DNS Providers**, **Users, Groups & Permissions**, and **API Tokens**. Each links onward to the deeper technical reference for readers who want it; the technical docs link back the other way too. The personal-access-token card (previously missed entirely) now has a help link. + +### Fixed +- The in-app docs viewer rendered every `docs/*.md` page with a garbled heading and a stray horizontal rule at the top — Jekyll front matter (meant only for the GitHub Pages build) was never stripped before being handed to the markdown renderer. Also fixed: cross-doc links never resolved in-app, since this viewer serves docs at `/docs/` with no `.html` suffix — they're now rewritten to the correct in-app URL (by registered slug, falling back to the doc's real filename), the same way image paths already were. + +Bumps to v1.1.13. ### Fixed - The host edit form's "Parent Wildcard" option stayed greyed out even when a valid wildcard actually existed for that host, so an already-created host could never be switched onto one from the edit modal (only brand-new hosts, via the field's `keyup` handler, ever saw it become available). The underlying `/host/lookup/:item` check also had the same self-match issue as the recently-fixed backend bug: it resolved an already-existing host to its own record instead of a sibling wildcard. Added a dedicated `/host/wildcard-parent/:item` endpoint that checks both directions, and the edit form now actually runs the check when it opens. @@ -88,7 +96,8 @@ First tagged release. Establishes the `vX.Y.Z` tag convention that the in-app up - Standalone backup script (`ops/backup.sh`) for deployments not using theta-env's orchestrator — snapshots Redis and `./config`, with retention. - Admin-only in-app banner that checks GitHub releases every 24h and surfaces available updates. -[Unreleased]: https://github.com/theta42/proxy/compare/v1.1.12...HEAD +[Unreleased]: https://github.com/theta42/proxy/compare/v1.1.13...HEAD +[1.1.13]: https://github.com/theta42/proxy/compare/v1.1.12...v1.1.13 [1.1.12]: https://github.com/theta42/proxy/compare/v1.1.11...v1.1.12 [1.1.11]: https://github.com/theta42/proxy/compare/v1.1.10...v1.1.11 [1.1.10]: https://github.com/theta42/proxy/compare/v1.1.9...v1.1.10 diff --git a/docs/architecture.md b/docs/architecture.md index ad1d7d9..29f7a04 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,6 +8,11 @@ description: How the proxy's OIDC client, LDAP client, and OpenResty routing fit [← Back to Home](index.html) +> Looking for a plainer explanation of hosts, HTTPS, or the local +> permission model instead of internals? See +> [Hosts & HTTPS](concepts-hosts.html) and +> [Users, Groups & Permissions](concepts-access.html). + ## System Overview The proxy system consists of three main components working together to provide high-performance reverse proxying with automated SSL management. diff --git a/docs/concepts-access.md b/docs/concepts-access.md new file mode 100644 index 0000000..76c9aa8 --- /dev/null +++ b/docs/concepts-access.md @@ -0,0 +1,75 @@ +--- +layout: default +title: Users, Groups & Permissions +description: A plain-language guide to local admin accounts, groups, and the domain-scoped permission model in theta42/proxy. +--- + +# Users, Groups & Permissions + +This page explains, in plain language, who can manage what in this app. For +the deeper system-design detail, see [Architecture](architecture.html). + +## Two different ways to log in + +Most people who use apps you've proxied through this app never see this +app's own login at all — they use whatever authentication you set up on +the *individual host* (basic auth, or single sign-on through your SSO +Manager). This page is about a different, smaller group: the people who +manage the proxy itself — adding hosts, registering DNS providers, and so +on. + +There are two ways someone gets into the proxy's own management UI: + +- **A local account**, created on the **Users** page — a username and + password specific to this app. +- **Single sign-on**, if you've connected this proxy to an SSO Manager (or + another OIDC provider) — the same login your other connected apps use. + +Either way, once logged in, what they're actually *allowed to do* here is +controlled by permissions, described below. + +## Groups + +A **group** here is just a named list of local usernames, used to grant +the same permission to several people at once instead of one at a time. +If you're using SSO instead of local accounts, group membership normally +comes from your identity provider instead — local groups exist mainly for +the local-account case. + +## Permissions: scope + role + +Each **permission** entry grants one subject (a user or a group) one +**role**, at one **scope** — the two are independent choices: + +**Scope** — *where* the role applies: + +- **Domain** — only hosts under one specific domain (e.g. someone can + manage everything under `example.com`, but can't see or touch a + completely different domain you also proxy). +- **Global** — everywhere, across every domain this proxy manages. + +**Role** — *what* they can do within that scope: + +- **Viewer** — read-only. Can see hosts and their settings, but not + change anything. +- **Manager** — full control over hosts (create, edit, delete) within + that scope. +- **Admin** — same host control as Manager, **plus**, but *only when + granted at Global scope*, the ability to manage other people's + permissions, DNS providers, and local user accounts. An Admin role + granted at Domain scope instead of Global behaves exactly like Manager + for that one domain — it does not unlock those extra admin-only pages. + +In practice: give someone **Manager** on just the domain(s) they're +responsible for to delegate day-to-day host management without handing +them the keys to everything. Reserve **Global Admin** for people who +should be able to change anything, anywhere, including who else has +access. + +## Want more detail? + +This page doesn't cover the exact permission-checking implementation or +how SSO group membership maps into this system internally — for that, see +[Architecture](architecture.html). + +[← Back to Home](index.html) diff --git a/docs/concepts-api-tokens.md b/docs/concepts-api-tokens.md new file mode 100644 index 0000000..5efe7d4 --- /dev/null +++ b/docs/concepts-api-tokens.md @@ -0,0 +1,60 @@ +--- +layout: default +title: API Tokens +description: A plain-language guide to personal access tokens in theta42/proxy. +--- + +# API Tokens + +This page explains what an API token is and when you'd want one. For the +full list of API endpoints a token can call, see the +[API reference](api.html). + +## What's an API token, in plain terms? + +Normally, you interact with this app by logging in through a web browser. +An **API token** (also called a personal access token, or PAT) is an +alternative way in — a long, random string that a script, a scheduled job, +or another program can use instead of a username and password, to act on +your behalf without a human typing a login in each time. + +If you've ever set up a script to talk to GitHub, GitLab, or a similar +service using a "token" instead of your real password, this is the same +idea. + +## When would you actually need one? + +Most people never need to create one of these — you'll only want a token +if you're automating something, for example: + +- A script that registers or updates hosts automatically (say, spinning up + a new service and wanting the proxy entry created for it without a + manual step). +- A monitoring or backup job that checks this app's health via its API. +- A configuration-management tool that keeps your host list in sync with + something else. + +If you're not doing any of that, you don't need an API token — just log in +normally through the web UI. + +## How it works + +Create a token from your Profile page, give it a name so you remember what +it's for later, and optionally an expiry. You'll be shown the token's +value **exactly once** — copy it somewhere safe immediately, because it +can't be viewed again afterward (only revoked or rotated). Whatever script +or tool you're using it with sends it along with each request, the same +way a browser sends your login session. + +A token acts **as you**, with **your** [permissions](concepts-access.html) +— if you're only a Manager on one domain, a token you create can't touch +any other domain either. If you ever suspect a token has leaked (ended up +somewhere it shouldn't have, like a public script or log file), revoke it +immediately from your Profile page; it stops working right away. + +## Want more detail? + +This page doesn't attempt to list every API endpoint or show request/ +response examples — for that, see the full [API reference](api.html). + +[← Back to Home](index.html) diff --git a/docs/concepts-dns.md b/docs/concepts-dns.md new file mode 100644 index 0000000..23e0d62 --- /dev/null +++ b/docs/concepts-dns.md @@ -0,0 +1,49 @@ +--- +layout: default +title: DNS Providers +description: A plain-language guide to why theta42/proxy needs a DNS provider, and only for wildcard certificates. +--- + +# DNS Providers + +This page explains, in plain language, what a "DNS provider" is for in this +app and when you actually need one. For setup steps, see +[Installation](installation.html). + +## Do you need this at all? + +**Only if you want a [wildcard host](concepts-hosts.html)** (something like +`*.example.com` covering every subdomain with one certificate). A normal, +single-name host doesn't need a DNS provider configured at all — skip this +page entirely if that's all you're setting up. + +## Why a wildcard cert needs this extra step + +To prove you actually own `example.com` before issuing a certificate that +covers *every* possible subdomain of it, Let's Encrypt needs to see a +specific, temporary DNS record appear on that domain — something only the +real owner of the domain could add. A normal single-host certificate +doesn't need this because it can prove ownership a simpler way (by +responding to a web request instead). + +So: to get a wildcard certificate, this app needs to be able to add (and +later remove) that one temporary DNS record on your domain automatically, +which means it needs your domain registrar or DNS host's API credentials — +that's what registering a **DNS provider** here does. + +## What you're actually giving it access to + +A DNS provider entry only needs enough access to add/remove TXT records — +it's not given your registrar account's full login, and it can't do +anything to your domain besides that one narrow task (and, for some +providers, keeping a dynamic A record updated if you use that feature +separately). Check your specific provider's page in the +[Installation guide](installation.html) for exactly what kind of +credential to generate and how narrowly you can scope it. + +## Want more detail? + +For exact setup steps per provider (Cloudflare, DigitalOcean, Porkbun, +DuckDNS, etc.), see [Installation](installation.html). + +[← Back to Home](index.html) diff --git a/docs/concepts-hosts.md b/docs/concepts-hosts.md new file mode 100644 index 0000000..806bd53 --- /dev/null +++ b/docs/concepts-hosts.md @@ -0,0 +1,77 @@ +--- +layout: default +title: Hosts & HTTPS +description: A plain-language guide to hosts, HTTPS certificates, and wildcards in theta42/proxy. +--- + +# Hosts & HTTPS + +This page explains, in plain language, what a "host" is and how this app +gets you working HTTPS without you having to think about certificates. For +the deeper system-design detail, see [Architecture](architecture.html); for +step-by-step setup, see [Installation](installation.html). + +## What's a "host"? + +A **host** is one entry telling the proxy: "when someone requests *this* +public address, send them to *that* server." For example: requests for +`photos.example.com` get sent to the little box in your closet running your +photo app on port 8080. Each app or service you want to reach from outside +your network — a home automation dashboard, a media server, this proxy's +own management UI — gets its own host entry. + +Two settings on a host are easy to mix up: + +- **Incoming host name** — the public address people type in their + browser (`photos.example.com`). +- **Target IP/port** — where the proxy actually sends the request behind + the scenes (`10.0.0.5:8080`, or a hostname like `photo-server`). + +Everything else on the host form (traffic limits, access rules, +authentication) is optional — a bare host with just those two fields +already works. + +## HTTPS certificates: mostly automatic + +Every public website needs an HTTPS certificate so browsers show the lock +icon instead of a scary warning. This app gets one for you automatically +from [Let's Encrypt](https://letsencrypt.org) the first time a host is +actually requested — you don't manually request, install, or renew +anything for a normal host. This happens behind the scenes using a method +called **HTTP-01**, and it's the default for every new host. + +## Wildcards: one certificate for a whole family of hosts + +Sometimes you want *every* subdomain under one name to work — `app1.`, +`app2.`, `anything.example.com` — without registering each one by hand and +waiting for its own certificate. That's what a **wildcard** host does: a +single host entry named `*.example.com` gets one certificate that covers +the whole family at once. Setting one up needs one extra piece of +information the automatic method above doesn't need — see +[DNS Providers](concepts-dns.html) for why. + +Once a wildcard exists, you have two ways to actually use it: + +- **Register nothing else, and turn on "Match any subdomain"** on the + wildcard host itself — *any* subdomain that doesn't already have its own + entry gets automatically routed to the wildcard's target the first time + it's requested. Convenient, but it means literal typos and random scan + traffic get routed too, not just the subdomains you meant to use. +- **Register each subdomain as its own host, as a "Parent Wildcard" + child** — more setup, but each subdomain can point at a different + target/server while still reusing the one wildcard certificate instead + of getting its own. This is the recommended default and is what + "Match only subdomains defined here" (the host form's default) does. + +You'll see the **"Parent Wildcard"** option light up automatically on the +host form whenever the name you're entering already has a matching +wildcard available to reuse — including the wildcard's own bare base +domain (e.g. `example.com` itself, not just `something.example.com`). + +## Want more detail? + +This page skips the system-internals (Redis, OpenResty, the lookup service) +and the exact install steps. For those, see +[Architecture](architecture.html) and [Installation](installation.html). + +[← Back to Home](index.html) diff --git a/docs/installation.md b/docs/installation.md index 32d6c6c..4b6e661 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,6 +8,10 @@ description: Installing the proxy — Docker, bare metal, or as part of the unif [← Back to Home](index.html) +> Looking for a plainer explanation of hosts, HTTPS, and DNS providers +> instead of install steps? See [Hosts & HTTPS](concepts-hosts.html) and +> [DNS Providers](concepts-dns.html). + ## Quick Install (Recommended) For modern Debian-based systems (Ubuntu 20.04+, Debian 11+): diff --git a/nodejs/api.md b/nodejs/api.md index f9b66ad..cc4e8b8 100755 --- a/nodejs/api.md +++ b/nodejs/api.md @@ -1,5 +1,10 @@ # API Documentation +> Looking for a plainer explanation of what API tokens are and when you'd +> want one, instead of a full endpoint reference? See +> [API Tokens](/docs/api-tokens) (in-app) or +> [concepts-api-tokens.md](../docs/concepts-api-tokens.md) (repo). + All API endpoints require authentication unless otherwise noted. Three authentication methods are supported: diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index c7d7352..9a5985b 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxy-api", - "version": "1.1.12", + "version": "1.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxy-api", - "version": "1.1.12", + "version": "1.1.13", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", diff --git a/nodejs/package.json b/nodejs/package.json index ca5b5bf..ac2dd9e 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "proxy-api", - "version": "1.1.12", + "version": "1.1.13", "private": true, "author": [ { diff --git a/nodejs/routes/docs.js b/nodejs/routes/docs.js index d50eadf..8fc77cc 100644 --- a/nodejs/routes/docs.js +++ b/nodejs/routes/docs.js @@ -33,6 +33,15 @@ const values = { // An explicit slug -> file allowlist, never a user-suppliable path, so // there's no way to make this read outside the doc set below. const DOCS = { + // Plain-language "what is this and why would I use it" guides -- linked + // directly from the relevant card in the UI (see the help icon on each + // card). Each links onward to the deeper technical doc below for readers + // who want the system-design/protocol-level detail. + hosts: {title: 'Hosts & HTTPS', file: path.join(__dirname, '../../docs/concepts-hosts.md')}, + dns: {title: 'DNS Providers', file: path.join(__dirname, '../../docs/concepts-dns.md')}, + access: {title: 'Users, Groups & Permissions', file: path.join(__dirname, '../../docs/concepts-access.md')}, + 'api-tokens': {title: 'API Tokens', file: path.join(__dirname, '../../docs/concepts-api-tokens.md')}, + overview: {title: 'Overview', file: path.join(__dirname, '../../README.md')}, changelog: {title: 'Changelog', file: path.join(__dirname, '../../CHANGELOG.md')}, deployment: {title: 'Deployment', file: path.join(__dirname, '../../DEPLOYMENT.md')}, @@ -54,6 +63,42 @@ function fixImagePaths(html) { return html.replace(/(["(])docs\/images\//g, '$1/docs/images/'); } +// docs/*.md files (not the repo-root README/CHANGELOG/api.md) carry Jekyll +// front matter for the GitHub Pages build and a "← Back to Home" link back +// to that site's index -- both meaningless here (this viewer has its own +// doc-list sidebar, docs_page.ejs) and, worse, marked() doesn't know front +// matter isn't regular markdown: it rendered as a garbled heading + stray +//
at the top of every page. Strip both before rendering. +function stripJekyllCruft(content) { + return content + .replace(/^---\n[\s\S]*?\n---\n/, '') + .replace(/^\s*\[← Back to Home\]\([^)]*\)\s*\n/m, ''); +} + +// Docs cross-link each other as ".html" (correct for the Jekyll/GitHub +// Pages build, which is what these same .md files also feed) and +// "index.html" for the docs home -- neither resolves here, where a doc lives +// at /docs/ with no .html suffix. Rewrite known doc links to the +// in-app route, same idea as fixImagePaths() above. Only touches slugs that +// actually exist, so an unrelated "foo.html" link is left alone. +// Docs are also linked by their real filename stem (e.g. "concepts-hosts.html" +// for docs/concepts-hosts.md) -- the correct, working link on the Jekyll/ +// GitHub Pages build, where the URL IS the filename stem. That doesn't match +// this viewer's own short slugs (DOCS keys, e.g. "hosts"), so also resolve by +// filename as a fallback -- one link written in a doc works correctly on +// both targets, rather than needing two different link forms. +const slugByFilename = Object.fromEntries( + Object.entries(DOCS).map(([slug, d]) => [path.basename(d.file, '.md'), slug]) +); +function fixDocLinks(html) { + return html + .replace(/href="index\.html"/g, 'href="/docs"') + .replace(/href="([a-z0-9-]+)\.html"/g, (match, name) => { + const slug = DOCS[name] ? name : slugByFilename[name]; + return slug ? `href="/docs/${slug}"` : match; + }); +} + router.use(docsLimiter); router.get('/', function(req, res) { @@ -73,7 +118,7 @@ router.get('/search', function(req, res) { const results = []; for (const [slug, doc] of Object.entries(DOCS)) { try { - const content = fs.readFileSync(doc.file, 'utf8'); + const content = stripJekyllCruft(fs.readFileSync(doc.file, 'utf8')); const matchLine = content.split('\n').find(line => line.toLowerCase().includes(qLower)); if (matchLine) { results.push({slug, title: doc.title, snippet: matchLine.trim().slice(0, 200)}); @@ -89,13 +134,13 @@ router.get('/:slug', function(req, res, next) { if (!doc) return next({status: 404, message: 'Doc not found'}); try { - const content = fs.readFileSync(doc.file, 'utf8'); + const content = stripJekyllCruft(fs.readFileSync(doc.file, 'utf8')); res.render('docs_page', { ...values, docs: docList, currentSlug: req.params.slug, docTitle: doc.title, - docHtml: fixImagePaths(marked(content)), + docHtml: fixDocLinks(fixImagePaths(marked(content))), }); } catch (error) { next(error); diff --git a/nodejs/views/dns.ejs b/nodejs/views/dns.ejs index 2593c50..126462e 100644 --- a/nodejs/views/dns.ejs +++ b/nodejs/views/dns.ejs @@ -125,7 +125,7 @@ Add DNS Provider - + @@ -227,7 +227,7 @@
Dynamic A Records - + This server's public IP: diff --git a/nodejs/views/groups.ejs b/nodejs/views/groups.ejs index f064fa7..6f5e0f9 100644 --- a/nodejs/views/groups.ejs +++ b/nodejs/views/groups.ejs @@ -86,7 +86,7 @@
Add Group - +
diff --git a/nodejs/views/hosts.ejs b/nodejs/views/hosts.ejs index fe52662..9a61c93 100755 --- a/nodejs/views/hosts.ejs +++ b/nodejs/views/hosts.ejs @@ -419,7 +419,7 @@ Proxy List - +
diff --git a/nodejs/views/permissions.ejs b/nodejs/views/permissions.ejs index ae1d6d2..7abdc37 100644 --- a/nodejs/views/permissions.ejs +++ b/nodejs/views/permissions.ejs @@ -85,7 +85,7 @@
Add Permission - +
@@ -141,7 +141,7 @@ Permissions - + diff --git a/nodejs/views/profile.ejs b/nodejs/views/profile.ejs index fa4c41f..6815399 100644 --- a/nodejs/views/profile.ejs +++ b/nodejs/views/profile.ejs @@ -207,7 +207,9 @@
-
New API Token
+
New API Token + +

A personal access token lets scripts and services call the proxy management API as you, with your permissions. Treat it like a password.

diff --git a/nodejs/views/users.ejs b/nodejs/views/users.ejs index 47ef5d4..b5b636a 100755 --- a/nodejs/views/users.ejs +++ b/nodejs/views/users.ejs @@ -66,7 +66,7 @@ Add New User - +
@@ -108,7 +108,7 @@ User List - +