Skip to content

IDP register UX polish (#284) — relaxed username, WebID preview, visual pass#285

Merged
melvincarvalho merged 2 commits into
JavaScriptSolidServer:gh-pagesfrom
melvincarvalho:issue-284-register-ux
Apr 22, 2026
Merged

IDP register UX polish (#284) — relaxed username, WebID preview, visual pass#285
melvincarvalho merged 2 commits into
JavaScriptSolidServer:gh-pagesfrom
melvincarvalho:issue-284-register-ux

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

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

  • Full suite: 379/379 pass (no test count change — this is UI plumbing).
  • Smoke: `POST /idp/register username=alice-smith` (was rejected) now succeeds; pod created with `.jsonld` layout; `username=..` correctly rejected with the new error message.
  • Smoke: rendered HTML contains the relaxed `pattern`, the `var cfg = {…}` config, and the gradient header markup.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/idp/views.js Outdated
Comment on lines +563 to +570
// 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 || '',
});

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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');

Copilot uses AI. Check for mistakes.
Comment thread src/idp/views.js Outdated
Comment on lines +621 to +622
pattern="[a-z0-9]([a-z0-9._-]{1,30}[a-z0-9])?"
title="Lowercase letters, numbers, or . _ - (start and end alphanumeric, 3–32 chars)">

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)">

Copilot uses AI. Check for mistakes.
Comment thread src/idp/views.js Outdated
if (!input || !webEl || !storEl) return;

function render() {
var u = (input.value || '').trim().toLowerCase();

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
var u = (input.value || '').trim().toLowerCase();
var normalized = (input.value || '').toLowerCase();
if (input.value !== normalized) input.value = normalized;
var u = normalized.trim();

Copilot uses AI. Check for mistakes.
Comment thread src/idp/interactions.js Outdated
Comment on lines +402 to +406
// 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])?$/;

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/idp/interactions.js Outdated
Comment on lines +406 to +411
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));

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@melvincarvalho
melvincarvalho merged commit 325743e into JavaScriptSolidServer:gh-pages Apr 22, 2026
4 checks passed
melvincarvalho added a commit that referenced this pull request Apr 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IDP register UX polish — WebID preview, relaxed username, auto-login

2 participants