IDP register UX polish (#284) — relaxed username, WebID preview, visual pass#285
Conversation
Three of the four items from JavaScriptSolidServer#284. Auto-login is deferred to a follow-up PR because it touches OIDC interaction state and deserves its own review. 1. Relaxed username regex. ^[a-z0-9]+$ → ^[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?$ Accepts realistic shapes like alice-smith, alice.smith, alice_work. Must still start and end alphanumeric (no leading dot, no trailing separator), no embedded "..". The new pattern is a superset of the old, so no existing user is invalidated. 2. Live WebID preview. Embeds a small JSON config (baseUri, subdomainsEnabled, baseDomain) into the page; a 30-line client-side script renders the WebID + storage URL the user is about to claim, in real time as they type. handleRegisterGet now takes the same `issuer` arg as handleRegisterPost, plumbed via a small `previewContext` helper. 3. Visual polish. Gradient header (matches pilot's onboarding feel), monospace preview block with subtle slate background. All scoped to the register page via `.container.register` so other IDP pages are unchanged. Smoke test: registering alice-smith now succeeds; pod created at /alice-smith/ with the .jsonld layout from JavaScriptSolidServer#283.
There was a problem hiding this comment.
Pull request overview
Improves the Solid IdP registration experience by expanding allowed usernames, adding a live WebID/storage preview on the register page, and applying registration-only visual styling updates.
Changes:
- Relax username validation (client + server) and add a server-side rejection for
... - Add client-side live preview of WebID + storage URL using server-provided config.
- Apply register-page-only styling updates (gradient header, preview block styling).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/idp/views.js | Adds preview UI + script and register-page-scoped CSS; updates username input constraints. |
| src/idp/interactions.js | Plumbs preview context into GET/POST register handlers; updates server-side username validation. |
| src/idp/index.js | Updates route wiring to pass issuer into the GET register handler. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Embed the values the live preview needs. Treat as opaque strings — the | ||
| // template substitution is JSON-serialised, so quotes / scripts inside | ||
| // baseUri can't break out. | ||
| const previewConfig = JSON.stringify({ | ||
| baseUri: ctx.baseUri || '', | ||
| subdomainsEnabled: !!ctx.subdomainsEnabled, | ||
| baseDomain: ctx.baseDomain || '', | ||
| }); |
There was a problem hiding this comment.
previewConfig is embedded directly into an inline <script> via template substitution. JSON.stringify() alone doesn’t prevent breaking out of the script context (e.g., a value containing </script> or U+2028/U+2029 can terminate the script / change parsing). Prefer escaping < (and line separators) before embedding, or move the JSON into a <script type="application/json" id="preview-config"> tag and read it via textContent.
| // Embed the values the live preview needs. Treat as opaque strings — the | |
| // template substitution is JSON-serialised, so quotes / scripts inside | |
| // baseUri can't break out. | |
| const previewConfig = JSON.stringify({ | |
| baseUri: ctx.baseUri || '', | |
| subdomainsEnabled: !!ctx.subdomainsEnabled, | |
| baseDomain: ctx.baseDomain || '', | |
| }); | |
| // Embed the values the live preview needs. Escape characters that are | |
| // unsafe in inline <script> contexts so values like "</script>" or | |
| // U+2028/U+2029 cannot break parsing when template-substituted. | |
| const previewConfig = JSON.stringify({ | |
| baseUri: ctx.baseUri || '', | |
| subdomainsEnabled: !!ctx.subdomainsEnabled, | |
| baseDomain: ctx.baseDomain || '', | |
| }) | |
| .replace(/</g, '\\u003c') | |
| .replace(/\u2028/g, '\\u2028') | |
| .replace(/\u2029/g, '\\u2029'); |
| pattern="[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?" | ||
| title="Lowercase letters, numbers, or . _ - (start and end alphanumeric, 3–32 chars)"> |
There was a problem hiding this comment.
The HTML pattern allows .. sequences, but the server-side validation rejects usernames containing ... This means the client-side and server-side validation rules are currently inconsistent (and the title implies stronger rules than the browser will enforce). Consider adding a negative lookahead to the pattern (if supported) or doing a small client-side check that matches the server behavior.
| pattern="[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?" | |
| title="Lowercase letters, numbers, or . _ - (start and end alphanumeric, 3–32 chars)"> | |
| pattern="(?!.*\\.\\.)[a-z0-9](?:[a-z0-9._-]{1,30}[a-z0-9])?" | |
| title="Lowercase letters, numbers, or . _ - (start and end alphanumeric, 3–32 chars, no consecutive dots)"> |
| if (!input || !webEl || !storEl) return; | ||
|
|
||
| function render() { | ||
| var u = (input.value || '').trim().toLowerCase(); |
There was a problem hiding this comment.
The preview lowercases the typed username (toLowerCase()), but POST /idp/register rejects uppercase usernames. This can produce a misleading preview (showing a valid-looking WebID for an input that will fail server-side). Either avoid lowercasing in the preview, or actively normalize the input value to lowercase on the client (and mirror that on the server if desired).
| var u = (input.value || '').trim().toLowerCase(); | |
| var normalized = (input.value || '').toLowerCase(); | |
| if (input.value !== normalized) input.value = normalized; | |
| var u = normalized.trim(); |
| // can contain dot, dash, underscore — covers `alice-smith`, `alice.smith`, | ||
| // `alice_work`, and so on. No leading/trailing separators (avoids the | ||
| // `.hidden` / trailing-dot footguns), no `..` (path traversal hygiene | ||
| // even though storage already guards against it). | ||
| const usernameRegex = /^[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?$/; |
There was a problem hiding this comment.
In subdomain mode, allowing . (and _) in usernameRegex can create pods that aren’t addressable as a single-level subdomain. server.js only sets request.podName for subdomains that don’t contain dots, so usernames like alice.smith will produce a WebID at alice.smith.<baseDomain> that the server won’t route as a pod subdomain. Consider tightening validation when request.subdomainsEnabled && request.baseDomain (e.g., disallow ./_) and align the HTML pattern accordingly.
| const usernameRegex = /^[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?$/; | ||
| if (!usernameRegex.test(username)) { | ||
| return reply.type('text/html').send(registerPage(uid, 'Username must contain only lowercase letters and numbers', null, inviteOnly)); | ||
| return reply.type('text/html').send(registerPage(uid, 'Username must be lowercase letters, numbers, or . _ - (start and end alphanumeric)', null, inviteOnly, ctx)); | ||
| } | ||
| if (username.includes('..')) { | ||
| return reply.type('text/html').send(registerPage(uid, 'Username cannot contain ".."', null, inviteOnly, ctx)); |
There was a problem hiding this comment.
There are existing test/idp.test.js integration tests for IdP flows, but the updated /idp/register validation logic (new regex + explicit .. rejection + new error messages) isn’t covered. Adding a couple of tests for accepted usernames (alice-smith, alice.smith, etc.) and rejected ones (.., leading/trailing separators, and subdomain-mode restrictions) would help prevent regressions.
1. XSS-safe inline script. previewConfig is now escaped against the
three sequences that can break out of an inline <script> context:
`</script>` (via `<` → \u003c), and the U+2028 / U+2029 line
separators that JS treats as line terminators. Plain JSON.stringify
doesn't handle these on its own.
2. Client/server validation alignment. The HTML pattern now mirrors
the server's "..-rejected" rule via a negative lookahead, and in
subdomain mode it tightens to alphanumeric + hyphen only (matching
server-side behaviour added below). title text updated to match.
3. Subdomain-mode regex. server.js refuses to route multi-level
subdomains, so `alice.smith` would create a non-addressable pod.
handleRegisterPost now picks /^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])?$/
when request.subdomainsEnabled && request.baseDomain, and uses a
distinct error message so the user knows why dot/underscore is out.
4. Lowercase normalisation. The username field is now coerced to
lowercase as the user types, so the live preview matches what the
server will accept (it rejects uppercase outright).
5. Test coverage. Adds 9 path-mode register cases (accepted: alice,
alice-smith, alice.smith, alice_work; rejected: leading separator,
trailing separator, consecutive dots, uppercase, too-short) and 3
subdomain-mode cases (dash accepted; dot + underscore rejected).
Also picks up the .jsonld pod creation flow from JavaScriptSolidServer#283 transitively.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
325743e
into
JavaScriptSolidServer:gh-pages
A human visitor opening /idp, /idp/auth, or pasting an old register-page "Sign In" link used to land on a raw OIDC error (invalid_request / missing client_id). This adds the cheap, no-OIDC-risk half of #286: - New landingPage() view in src/idp/views.js: gradient header matching the register page (#284), Create Account button, sign-in note, issuer footer. Single primary CTA — standalone /idp/login is intentionally PR-B (needs oidc-provider session-minting). - GET /idp registered in src/idp/index.js. Auth bypass extended in server.js to cover /idp (and /idp?…) so the landing page is publicly accessible. - /idp/auth pulled out of the oidc-provider catch-all loop into its own guarded route: GET requests with no client_id now 302 to /idp; OIDC requests pass through unchanged. - Register page's "Already have an account?" link now points at /idp rather than /idp/auth (broken without client_id) — the link reads "Back to home" without an active interaction, "Sign In" with one. - Tests cover landing render, redirect-on-bare-/idp/auth, and that real OIDC requests (with client_id) still reach the provider. Closes the dead-link wart from #285. Standalone /idp/login + auto-login remain tracked under #286 as PR-B.
Fixes #284 (3 of 4 items — auto-login deferred to a follow-up).
Changes
1. Relaxed username regex.
Was `^[a-z0-9]+$` (no separators). Now `^a-z0-9?$` — accepts `alice-smith`, `alice.smith`, `alice_work`, etc. Must start and end alphanumeric (rules out `.hidden`, trailing dots), no embedded `..`. New pattern is a superset of the old, so no existing user is invalidated. The HTML `pattern` and `title` attributes match the server-side check.
2. Live WebID preview.
While the user types, the page renders the WebID and storage URL they're about to get:
```
WebID https://jss.live/alice-smith/profile/card.jsonld#me
Storage https://jss.live/alice-smith/
```
Implementation: `handleRegisterGet` now takes the same `issuer` arg as the POST handler (plumbed via a tiny `previewContext` helper). The view embeds a JSON config `{ baseUri, subdomainsEnabled, baseDomain }` and a ~30-line script wires the input `oninput` event to update the preview. Subdomain mode resolves to `alice-smith.example.com`; path mode to `example.com/alice-smith/`.
3. Visual pass.
Gradient header on the register card (matches pilot's onboarding feel), monospace preview block on slate background. All scoped to `.container.register` so the other IDP pages (login, consent, passkey prompt, error) are unchanged.
Out of scope
Test plan