feat(ui): auth-gate the landing, add Help page and Home nav
- Landing shows the tool tiles only when signed in and a sign-in card
otherwise; the login callback now returns to /.
- New public /help page: intro plus a per-tool Carbon accordion.
- Nav restructured to Home, {services}, divider, Help, Sign In/Out in
both the header and the side nav; the header right keeps only the
identity chip. Side-nav logo enlarged, border removed, spacing
equalised. Sign-out modal no longer flashes before it is defined.
- Per-deploy asset versioning (?v=) with immutable asset caching and
no-store HTML; help step lists show their numbers again.
- Tests updated and extended (48 total).
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Help · YOUreka Tools</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@carbon/styles@1.69.0/css/styles.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<link rel="icon" type="image/png" href="/assets/youreka-favicon.png">
|
||||||
|
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/ui-shell.min.js"></script>
|
||||||
|
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/modal.min.js"></script>
|
||||||
|
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/accordion.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="app cds--g100">
|
||||||
|
<!--app-header-->
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<div class="page-head">
|
||||||
|
<div>
|
||||||
|
<h1>Help</h1>
|
||||||
|
<p class="sub">How to use the YOUreka Tools</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-intro">
|
||||||
|
YOUreka Tools is a set of field utilities for YOUreka ID domains. Sign in
|
||||||
|
with your YOUreka account to use the Image Cropper and the Fingerprint
|
||||||
|
Agent. Everything runs on this machine — nothing is uploaded except
|
||||||
|
the artifacts you explicitly save. Permissions are per domain, so an
|
||||||
|
administrator must grant you access to the domains you work on.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<cds-accordion>
|
||||||
|
<cds-accordion-item title="Image Cropper">
|
||||||
|
<p>Review, crop and save identity photos submitted for YOUreka ID domain profiles.</p>
|
||||||
|
<ol class="help-steps">
|
||||||
|
<li>Open <strong>Image Cropper</strong> from the landing page or the menu.</li>
|
||||||
|
<li>Load a picture with <strong>Load next queued</strong>, or paste a queue item with <strong>Load by upload ID</strong>.</li>
|
||||||
|
<li>Check the details (phone, domain, agent) and crop the photo in the editor.</li>
|
||||||
|
<li>If the background needs cleaning, choose <strong>Requires cleaning? → Yes</strong>, then click <strong>Clean background</strong>.</li>
|
||||||
|
<li>To keep it, click <strong>Accept picture</strong>, then <strong>Crop and upload picture</strong>. To reject it, click <strong>Reject picture</strong>, tick the reasons (and fill in <em>Others</em> if asked), then <strong>Send review</strong>.</li>
|
||||||
|
</ol>
|
||||||
|
<p class="help-note">The cropper requires the <em>crop_image</em> permission. If you see “No access”, ask a domain administrator to grant it.</p>
|
||||||
|
</cds-accordion-item>
|
||||||
|
|
||||||
|
<cds-accordion-item title="Fingerprint Agent">
|
||||||
|
<p>Enroll staff fingerprints with the scanner attached to this laptop. Captures are authorized per domain.</p>
|
||||||
|
<ol class="help-steps">
|
||||||
|
<li>Open <strong>Fingerprint Agent</strong> from the landing page or the menu.</li>
|
||||||
|
<li>Choose the domain, enter the staff reference and the OTP, then look up the profile.</li>
|
||||||
|
<li>Confirm the profile is the right person.</li>
|
||||||
|
<li>Follow the on-screen prompts and place each finger on the attached scanner when asked.</li>
|
||||||
|
<li>Review the captured prints and save.</li>
|
||||||
|
</ol>
|
||||||
|
<p class="help-note">The scanner runs locally through the <em>fpagent</em> service; scans never leave this machine. Domain permissions are re-checked periodically and cached for a few minutes.</p>
|
||||||
|
</cds-accordion-item>
|
||||||
|
</cds-accordion>
|
||||||
|
|
||||||
|
<p class="help-note">
|
||||||
|
Your session lasts 30 days. Use <strong>Sign out</strong> in the menu to end it early.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="container">
|
||||||
|
<span>YOUreka Tools · Help</span>
|
||||||
|
<span>Need more detail? Ask your YOUreka ID administrator.</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -26,11 +26,13 @@ cds-progress-bar:not(:defined) {
|
|||||||
/* Carbon styles the shadow anchor inside tiles, not the host — give the host
|
/* Carbon styles the shadow anchor inside tiles, not the host — give the host
|
||||||
a real box so grid columns and click targets work correctly. */
|
a real box so grid columns and click targets work correctly. */
|
||||||
cds-tile, cds-clickable-tile { display: block; }
|
cds-tile, cds-clickable-tile { display: block; }
|
||||||
|
/* an undefined modal renders its header/body/footer inline — hide it until defined */
|
||||||
|
cds-modal:not(:defined) { display: none; }
|
||||||
|
|
||||||
/* ── header + side nav (Carbon ui-shell) ────────────────────────────────── */
|
/* ── header + side nav (Carbon ui-shell) ────────────────────────────────── */
|
||||||
cds-header:not(:defined), cds-side-nav:not(:defined) { visibility: hidden; }
|
cds-header:not(:defined), cds-side-nav:not(:defined) { visibility: hidden; }
|
||||||
.brand-inner { display: inline-flex; align-items: center; gap: 0.5rem; }
|
.brand-inner { display: inline-flex; align-items: center; gap: 0.5rem; }
|
||||||
.brand-logo { display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--cds-border-subtle, #e0e0e0); border-radius: 4px; padding: 2px; }
|
.brand-logo { display: inline-flex; align-items: center; justify-content: center; }
|
||||||
.brand-logo img { display: block; }
|
.brand-logo img { display: block; }
|
||||||
.brand-suffix { color: var(--cds-text-secondary, #525252); font-weight: 400; }
|
.brand-suffix { color: var(--cds-text-secondary, #525252); font-weight: 400; }
|
||||||
|
|
||||||
@@ -61,13 +63,13 @@ cds-side-nav[expanded] { pointer-events: auto; }
|
|||||||
}
|
}
|
||||||
.user-chip { display: inline-flex; align-items: center; gap: 0.45rem; color: var(--cds-text-primary, #161616); font-size: 13px; }
|
.user-chip { display: inline-flex; align-items: center; gap: 0.45rem; color: var(--cds-text-primary, #161616); font-size: 13px; }
|
||||||
.user-chip .avatar { width: 26px; height: 26px; border-radius: 50%; background: #0f62fe; color: #fff; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; }
|
.user-chip .avatar { width: 26px; height: 26px; border-radius: 50%; background: #0f62fe; color: #fff; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; }
|
||||||
.linklike { background: none; border: none; padding: 0; color: var(--cds-link-primary, #0f62fe); font-weight: 400; cursor: pointer; font-size: 13px; font-family: inherit; }
|
|
||||||
.linklike:hover { color: var(--cds-link-primary-hover, #0043ce); text-decoration: underline; }
|
|
||||||
.linklike[disabled] { color: var(--cds-text-disabled, #c6c6c6); cursor: default; }
|
|
||||||
|
|
||||||
/* logo above the mobile side-nav items */
|
/* logo above the mobile side-nav items */
|
||||||
.side-nav-brand { display: flex; align-items: center; gap: 0.75rem; padding: 1rem; color: var(--cds-text-primary, #161616); font-weight: 600; font-size: 14px; border-bottom: 1px solid var(--cds-border-subtle, #e0e0e0); }
|
.side-nav-brand { display: flex; align-items: center; gap: 0.75rem; padding: 2rem 1rem 1rem; color: var(--cds-text-primary, #161616); font-weight: 600; font-size: 14px; }
|
||||||
.side-nav-brand img { border: 1px solid var(--cds-border-subtle, #e0e0e0); border-radius: 6px; padding: 2px; display: block; }
|
.side-nav-brand img { display: block; }
|
||||||
|
|
||||||
|
/* separator between the service links and Help in the header nav */
|
||||||
|
.header-nav-divider { width: 1px; align-self: stretch; margin: 0.7rem 0.25rem; background: var(--cds-border-subtle, #e0e0e0); }
|
||||||
|
|
||||||
/* ── layout ─────────────────────────────────────────────────────────────── */
|
/* ── layout ─────────────────────────────────────────────────────────────── */
|
||||||
.container { width: 100%; max-width: 1136px; margin: 0 auto; padding: 0 1.5rem; }
|
.container { width: 100%; max-width: 1136px; margin: 0 auto; padding: 0 1.5rem; }
|
||||||
@@ -114,6 +116,14 @@ main.container { flex: 1 0 auto; padding-top: 1.5rem; padding-bottom: 1.5rem; }
|
|||||||
.tool-card .go { margin-top: auto; color: var(--cds-link, #0f62fe); font-weight: 600; font-size: 13px; display: inline-flex; align-items: center; gap: 0.3rem; }
|
.tool-card .go { margin-top: auto; color: var(--cds-link, #0f62fe); font-weight: 600; font-size: 13px; display: inline-flex; align-items: center; gap: 0.3rem; }
|
||||||
.tool-card .go svg { width: 14px; height: 14px; }
|
.tool-card .go svg { width: 14px; height: 14px; }
|
||||||
|
|
||||||
|
/* ── help page ──────────────────────────────────────────────────────────── */
|
||||||
|
cds-accordion { display: block; max-width: 720px; }
|
||||||
|
.help-intro { max-width: 720px; color: var(--cds-text-secondary, #525252); margin: 0 0 1.5rem; font-size: 14px; }
|
||||||
|
.help-steps { list-style: decimal; padding-left: 1.25rem; margin: 0.5rem 0; }
|
||||||
|
.help-steps li { margin-bottom: 0.35rem; }
|
||||||
|
.help-steps li::marker { color: var(--cds-text-secondary, #525252); }
|
||||||
|
.help-note { color: var(--cds-text-secondary, #525252); font-size: 13px; max-width: 720px; }
|
||||||
|
|
||||||
/* ── forms (Carbon-styled native controls) ──────────────────────────────── */
|
/* ── forms (Carbon-styled native controls) ──────────────────────────────── */
|
||||||
label { font-weight: 400; font-size: 12px; color: var(--cds-text-secondary, #525252); margin-bottom: 0.25rem; display: block; }
|
label { font-weight: 400; font-size: 12px; color: var(--cds-text-secondary, #525252); margin-bottom: 0.25rem; display: block; }
|
||||||
.form-control {
|
.form-control {
|
||||||
@@ -131,9 +141,10 @@ textarea.form-control { height: auto; padding: 0.6rem 0.75rem; }
|
|||||||
.btn {
|
.btn {
|
||||||
border-radius: 0; font-weight: 400; font-size: 14px;
|
border-radius: 0; font-weight: 400; font-size: 14px;
|
||||||
padding: 0.65rem 0.95rem; border: 1px solid transparent;
|
padding: 0.65rem 0.95rem; border: 1px solid transparent;
|
||||||
text-decoration: none; display: inline-block; cursor: pointer;
|
text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; cursor: pointer;
|
||||||
font-family: inherit; line-height: 1.2; text-align: center;
|
font-family: inherit; line-height: 1.2; text-align: center;
|
||||||
}
|
}
|
||||||
|
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||||
.btn-primary { background: var(--cds-button-primary, #0f62fe); border-color: var(--cds-button-primary, #0f62fe); color: #fff; }
|
.btn-primary { background: var(--cds-button-primary, #0f62fe); border-color: var(--cds-button-primary, #0f62fe); color: #fff; }
|
||||||
.btn-primary:hover, .btn-primary:focus { background: #0353e9; border-color: #0353e9; color: #fff; }
|
.btn-primary:hover, .btn-primary:focus { background: #0353e9; border-color: #0353e9; color: #fff; }
|
||||||
.btn-ghost { background: transparent; border-color: var(--cds-border-strong, #8d8d8d); color: var(--cds-text-primary, #161616); }
|
.btn-ghost { background: transparent; border-color: var(--cds-border-strong, #8d8d8d); color: var(--cds-text-primary, #161616); }
|
||||||
@@ -196,4 +207,5 @@ footer.foot a { color: var(--cds-text-secondary, #525252); }
|
|||||||
.message-card h1 { font-size: 1.25rem; font-weight: 600; margin: 0; }
|
.message-card h1 { font-size: 1.25rem; font-weight: 600; margin: 0; }
|
||||||
.message-icon { width: 52px; height: 52px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: var(--cds-support-error, #da1e28); color: #fff; margin-bottom: 0.4rem; }
|
.message-icon { width: 52px; height: 52px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: var(--cds-support-error, #da1e28); color: #fff; margin-bottom: 0.4rem; }
|
||||||
.message-icon.error svg { width: 24px; height: 24px; }
|
.message-icon.error svg { width: 24px; height: 24px; }
|
||||||
|
.message-icon.info { background: var(--cds-button-primary, #0f62fe); }
|
||||||
.message-card .btn { margin-top: 0.6rem; min-width: 200px; }
|
.message-card .btn { margin-top: 0.6rem; min-width: 200px; }
|
||||||
|
|||||||
+2
-28
@@ -22,36 +22,10 @@
|
|||||||
<main class="container">
|
<main class="container">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<h1>YOUreka Tools</h1>
|
<h1>YOUreka Tools</h1>
|
||||||
<p>Field utilities for YOUreka ID Domains. Sign in with your YOUreka account to use the tools that require it.</p>
|
<p>Field utilities for YOUreka ID Domains.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-grid">
|
<!--landing-view-->
|
||||||
<cds-clickable-tile href="/cropper">
|
|
||||||
<div class="tool-card">
|
|
||||||
<span class="tool-icon">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2v14a2 2 0 0 0 2 2h14"/><path d="M18 22V8a2 2 0 0 0-2-2H2"/></svg>
|
|
||||||
</span>
|
|
||||||
<h3>Image Cropper</h3>
|
|
||||||
<p>Review, crop and save identity photos submitted for YOUreka ID domain profiles.</p>
|
|
||||||
<span class="go">Open Image Cropper
|
|
||||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</cds-clickable-tile>
|
|
||||||
|
|
||||||
<cds-clickable-tile href="/fingerprint">
|
|
||||||
<div class="tool-card">
|
|
||||||
<span class="tool-icon">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a8 8 0 0 0-8 8c0 3.5 1 5.5 2 7"/><path d="M16 21a10 10 0 0 0 4-13 8 8 0 0 0-15.7-1"/><path d="M8 21a8 8 0 0 1-1-11 5 5 0 0 1 9.7 1.4A6 6 0 0 1 12 17"/><path d="M12 21a4 4 0 0 0 4-4 4 4 0 0 0-1.2-2.9"/></svg>
|
|
||||||
</span>
|
|
||||||
<h3>Fingerprint Agent</h3>
|
|
||||||
<p>Enroll staff fingerprints with the scanner attached to this laptop. Captures are authorized per domain.</p>
|
|
||||||
<span class="go">Open Fingerprint Agent
|
|
||||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</cds-clickable-tile>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="foot">
|
<footer class="foot">
|
||||||
|
|||||||
+109
-20
@@ -31,6 +31,18 @@ export const DOMAINS = [
|
|||||||
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Bumped once per process start, so every deployment serves fresh asset URLs.
|
||||||
|
export const DEPLOY_VERSION = Date.now().toString(36);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the per-deploy version to every /assets URL in a served page, so the
|
||||||
|
* browser fetches fresh CSS/JS/images after a deploy instead of reusing cached
|
||||||
|
* ones. Idempotent: URLs that already carry `?v=` are left alone.
|
||||||
|
*/
|
||||||
|
function versionAssets(html) {
|
||||||
|
return html.replace(/(["'(])(\/assets\/[^"')\s?]+)/g, `$1$2?v=${DEPLOY_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Express does not parse cookies; this is all we need.
|
// Express does not parse cookies; this is all we need.
|
||||||
@@ -66,7 +78,7 @@ function errorPage(res, status, title, message, actions = {}) {
|
|||||||
res
|
res
|
||||||
.status(status)
|
.status(status)
|
||||||
.type('html')
|
.type('html')
|
||||||
.send(`<!doctype html>
|
.send(versionAssets(`<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@@ -90,7 +102,7 @@ function errorPage(res, status, title, message, actions = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>`);
|
</html>`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -122,10 +134,13 @@ const appHeader = (session, active) => {
|
|||||||
const sideLink = (href, label, key, icon) =>
|
const sideLink = (href, label, key, icon) =>
|
||||||
`<cds-side-nav-link href="${href}"${key === active ? ' aria-current="page"' : ''}>${icon || ''}${label}</cds-side-nav-link>`;
|
`<cds-side-nav-link href="${href}"${key === active ? ' aria-current="page"' : ''}>${icon || ''}${label}</cds-side-nav-link>`;
|
||||||
const name = session ? (session.firstName || session.phone || '') : '';
|
const name = session ? (session.firstName || session.phone || '') : '';
|
||||||
const right = session
|
// Auth is a menu item now, so the header's right side only carries identity.
|
||||||
? `<span class="user-chip"><span class="avatar">${esc(name.trim().slice(0, 2).toUpperCase())}</span>${esc(name)}</span>
|
const headerAuth = session
|
||||||
<a class="linklike" href="/auth/logout" data-signout>Sign out</a>`
|
? '<cds-header-nav-item href="/auth/logout" data-signout>Sign out</cds-header-nav-item>'
|
||||||
: '<a class="linklike" href="/auth/login">Sign in</a>';
|
: '<cds-header-nav-item href="/auth/login">Sign in</cds-header-nav-item>';
|
||||||
|
const sideAuth = session
|
||||||
|
? `<cds-side-nav-link href="/auth/logout" data-signout>${SIDE_NAV_ICONS.logout}Sign out</cds-side-nav-link>`
|
||||||
|
: `<cds-side-nav-link href="/auth/login">${SIDE_NAV_ICONS.login}Sign in</cds-side-nav-link>`;
|
||||||
return `
|
return `
|
||||||
<cds-header aria-label="YOUreka Tools">
|
<cds-header aria-label="YOUreka Tools">
|
||||||
<cds-header-menu-button button-label-active="Close menu" button-label-inactive="Open menu" id="header-menu-button"></cds-header-menu-button>
|
<cds-header-menu-button button-label-active="Close menu" button-label-inactive="Open menu" id="header-menu-button"></cds-header-menu-button>
|
||||||
@@ -133,25 +148,30 @@ const appHeader = (session, active) => {
|
|||||||
<span class="brand-inner"><span class="brand-logo"><img src="/assets/youreka-logo.png" alt="" width="20" height="20"></span><span>YOUreka<span class="brand-suffix"> Tools</span></span></span>
|
<span class="brand-inner"><span class="brand-logo"><img src="/assets/youreka-logo.png" alt="" width="20" height="20"></span><span>YOUreka<span class="brand-suffix"> Tools</span></span></span>
|
||||||
</cds-header-name>
|
</cds-header-name>
|
||||||
<cds-header-nav>
|
<cds-header-nav>
|
||||||
${toolLink('/cropper', 'Image Cropper', 'cropper')}
|
${toolLink('/', 'Home', 'home')}
|
||||||
|
${session ? `${toolLink('/cropper', 'Image Cropper', 'cropper')}
|
||||||
${toolLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
|
${toolLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
|
||||||
|
<span class="header-nav-divider" aria-hidden="true"></span>` : ''}
|
||||||
|
${toolLink('/help', 'Help', 'help')}
|
||||||
|
${headerAuth}
|
||||||
</cds-header-nav>
|
</cds-header-nav>
|
||||||
${active === 'fingerprint' ? '<cds-tag id="agent-status" type="red" class="header-agent">Agent offline</cds-tag>' : ''}
|
${active === 'fingerprint' ? '<cds-tag id="agent-status" type="red" class="header-agent">Agent offline</cds-tag>' : ''}
|
||||||
<div class="header-right">${right}</div>
|
${session ? `<div class="header-right"><span class="user-chip"><span class="avatar">${esc(name.trim().slice(0, 2).toUpperCase())}</span>${esc(name)}</span></div>` : ''}
|
||||||
</cds-header>
|
</cds-header>
|
||||||
<cds-side-nav aria-label="Tools" id="side-nav">
|
<cds-side-nav aria-label="Tools" id="side-nav">
|
||||||
<div class="side-nav-brand">
|
<div class="side-nav-brand">
|
||||||
<img src="/assets/youreka-logo.png" alt="YOUreka" width="40" height="40">
|
<img src="/assets/youreka-logo.png" alt="YOUreka" width="56" height="56">
|
||||||
<span>YOUreka Tools</span>
|
|
||||||
</div>
|
</div>
|
||||||
<cds-side-nav-items>
|
<cds-side-nav-items>
|
||||||
${sideLink('/cropper', 'Image Cropper', 'cropper', SIDE_NAV_ICONS.crop)}
|
${sideLink('/', 'Home', 'home', SIDE_NAV_ICONS.home)}
|
||||||
|
${session ? `${sideLink('/cropper', 'Image Cropper', 'cropper', SIDE_NAV_ICONS.crop)}
|
||||||
${sideLink('/fingerprint', 'Fingerprint Agent', 'fingerprint', SIDE_NAV_ICONS.fingerprint)}
|
${sideLink('/fingerprint', 'Fingerprint Agent', 'fingerprint', SIDE_NAV_ICONS.fingerprint)}
|
||||||
<cds-side-nav-divider></cds-side-nav-divider>
|
<cds-side-nav-divider></cds-side-nav-divider>` : ''}
|
||||||
<cds-side-nav-link href="/auth/logout" data-signout>${SIDE_NAV_ICONS.logout}Sign out</cds-side-nav-link>
|
${sideLink('/help', 'Help', 'help', SIDE_NAV_ICONS.help)}
|
||||||
|
${sideAuth}
|
||||||
</cds-side-nav-items>
|
</cds-side-nav-items>
|
||||||
</cds-side-nav>
|
</cds-side-nav>
|
||||||
<cds-modal id="signout-modal" size="xs" aria-label="Sign out of YOUreka Tools">
|
${session ? `<cds-modal id="signout-modal" size="xs" aria-label="Sign out of YOUreka Tools">
|
||||||
<cds-modal-header>
|
<cds-modal-header>
|
||||||
<cds-modal-close-button></cds-modal-close-button>
|
<cds-modal-close-button></cds-modal-close-button>
|
||||||
<cds-modal-label>Account</cds-modal-label>
|
<cds-modal-label>Account</cds-modal-label>
|
||||||
@@ -159,8 +179,8 @@ const appHeader = (session, active) => {
|
|||||||
</cds-modal-header>
|
</cds-modal-header>
|
||||||
<cds-modal-body>You will need to authenticate again to use the tools.</cds-modal-body>
|
<cds-modal-body>You will need to authenticate again to use the tools.</cds-modal-body>
|
||||||
<cds-modal-footer>
|
<cds-modal-footer>
|
||||||
<cds-modal-footer-button kind="secondary" data-modal-close>Cancel</cds-modal-footer-button>
|
<cds-modal-footer-button kind="secondary" data-modal-close><svg slot="icon" width="16" height="16" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M17.4141 16L24 9.4141 22.5859 8 16 14.5859 9.4143 8 8 9.4141 14.5859 16 8 22.5859 9.4143 24 16 17.4141 22.5859 24 24 22.5859 17.4141 16z"/></svg>Cancel</cds-modal-footer-button>
|
||||||
<cds-modal-footer-button kind="danger" id="signout-confirm">Sign out</cds-modal-footer-button>
|
<cds-modal-footer-button kind="danger" id="signout-confirm"><svg slot="icon" width="16" height="16" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M6,30H18a2.0023,2.0023,0,0,0,2-2V25H18v3H6V4H18V7h2V4a2.0023,2.0023,0,0,0-2-2H6A2.0023,2.0023,0,0,0,4,4V28A2.0023,2.0023,0,0,0,6,30Z"/><path d="M20.586 20.586L24.172 17 10 17 10 15 24.172 15 20.586 11.414 22 10 28 16 22 22 20.586 20.586z"/></svg>Sign out</cds-modal-footer-button>
|
||||||
</cds-modal-footer>
|
</cds-modal-footer>
|
||||||
</cds-modal>
|
</cds-modal>
|
||||||
<script>
|
<script>
|
||||||
@@ -183,7 +203,7 @@ const appHeader = (session, active) => {
|
|||||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
|
||||||
else wire();
|
else wire();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>` : ''}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,8 +212,53 @@ const SIDE_NAV_ICONS = {
|
|||||||
crop: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M25,20H23V9H12V7H23a2,2,0,0,1,2,2Z"/><path d="M9,23V2H7V7H2V9H7V23a2,2,0,0,0,2,2H23v5h2V25h5V23Z"/></svg>',
|
crop: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M25,20H23V9H12V7H23a2,2,0,0,1,2,2Z"/><path d="M9,23V2H7V7H2V9H7V23a2,2,0,0,0,2,2H23v5h2V25h5V23Z"/></svg>',
|
||||||
fingerprint: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M7,5.21a.77.77,0,0,1-.46-1.38A15.46,15.46,0,0,1,16,1c2.66,0,6.48.45,9.5,2.62a.77.77,0,0,1,.18,1.07.78.78,0,0,1-1.08.17A15,15,0,0,0,16,2.53,14,14,0,0,0,7.5,5.05.74.74,0,0,1,7,5.21Z"/><path d="M28.23,12.26a.78.78,0,0,1-.63-.33C25.87,9.49,22.78,6.24,16,6.24a14,14,0,0,0-11.63,5.7.77.77,0,0,1-1.07.17A.76.76,0,0,1,3.15,11,15.54,15.54,0,0,1,16,4.71c5.61,0,9.81,2.08,12.84,6.34a.77.77,0,0,1-.19,1.07A.79.79,0,0,1,28.23,12.26Z"/><path d="M12.28,31a.78.78,0,0,1-.72-.49.75.75,0,0,1,.44-1c4.37-1.68,7-5.12,7-9.21a2.8,2.8,0,0,0-3-3c-1.86,0-2.76,1-3,3.35a4.27,4.27,0,0,1-4.52,3.83,4.27,4.27,0,0,1-4.32-4.59A11.71,11.71,0,0,1,16,8.39a12,12,0,0,1,12,11.93,18.66,18.66,0,0,1-1.39,6.5.78.78,0,0,1-1,.41.76.76,0,0,1-.41-1,17.25,17.25,0,0,0,1.27-5.91A10.45,10.45,0,0,0,16,9.92a10.18,10.18,0,0,0-10.38,10,2.77,2.77,0,0,0,2.79,3.06,2.74,2.74,0,0,0,3-2.48c.36-3.11,1.89-4.69,4.56-4.69a4.31,4.31,0,0,1,4.52,4.56c0,4.74-3,8.72-8,10.63A.92.92,0,0,1,12.28,31Z"/><path d="M19.77,30.28a.81.81,0,0,1-.52-.2.76.76,0,0,1,0-1.08,12.63,12.63,0,0,0,3.54-8.68c0-1.56-.48-6.65-6.7-6.65a6.83,6.83,0,0,0-4.94,1.87A6.17,6.17,0,0,0,9.32,20a.77.77,0,0,1-.77.76h0A.76.76,0,0,1,7.78,20,7.73,7.73,0,0,1,10,14.46a8.34,8.34,0,0,1,6-2.32c6.08,0,8.24,4.4,8.24,8.18A14.09,14.09,0,0,1,20.34,30,.75.75,0,0,1,19.77,30.28Z"/><path d="M8.66,27.74a14.14,14.14,0,0,1-1.56-.09.76.76,0,1,1,.17-1.52c2.49.28,4.45-.16,5.84-1.32a6.37,6.37,0,0,0,2.12-4.53.75.75,0,0,1,.82-.71.78.78,0,0,1,.72.81A7.89,7.89,0,0,1,14.09,26,8.2,8.2,0,0,1,8.66,27.74Z"/></svg>',
|
fingerprint: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M7,5.21a.77.77,0,0,1-.46-1.38A15.46,15.46,0,0,1,16,1c2.66,0,6.48.45,9.5,2.62a.77.77,0,0,1,.18,1.07.78.78,0,0,1-1.08.17A15,15,0,0,0,16,2.53,14,14,0,0,0,7.5,5.05.74.74,0,0,1,7,5.21Z"/><path d="M28.23,12.26a.78.78,0,0,1-.63-.33C25.87,9.49,22.78,6.24,16,6.24a14,14,0,0,0-11.63,5.7.77.77,0,0,1-1.07.17A.76.76,0,0,1,3.15,11,15.54,15.54,0,0,1,16,4.71c5.61,0,9.81,2.08,12.84,6.34a.77.77,0,0,1-.19,1.07A.79.79,0,0,1,28.23,12.26Z"/><path d="M12.28,31a.78.78,0,0,1-.72-.49.75.75,0,0,1,.44-1c4.37-1.68,7-5.12,7-9.21a2.8,2.8,0,0,0-3-3c-1.86,0-2.76,1-3,3.35a4.27,4.27,0,0,1-4.52,3.83,4.27,4.27,0,0,1-4.32-4.59A11.71,11.71,0,0,1,16,8.39a12,12,0,0,1,12,11.93,18.66,18.66,0,0,1-1.39,6.5.78.78,0,0,1-1,.41.76.76,0,0,1-.41-1,17.25,17.25,0,0,0,1.27-5.91A10.45,10.45,0,0,0,16,9.92a10.18,10.18,0,0,0-10.38,10,2.77,2.77,0,0,0,2.79,3.06,2.74,2.74,0,0,0,3-2.48c.36-3.11,1.89-4.69,4.56-4.69a4.31,4.31,0,0,1,4.52,4.56c0,4.74-3,8.72-8,10.63A.92.92,0,0,1,12.28,31Z"/><path d="M19.77,30.28a.81.81,0,0,1-.52-.2.76.76,0,0,1,0-1.08,12.63,12.63,0,0,0,3.54-8.68c0-1.56-.48-6.65-6.7-6.65a6.83,6.83,0,0,0-4.94,1.87A6.17,6.17,0,0,0,9.32,20a.77.77,0,0,1-.77.76h0A.76.76,0,0,1,7.78,20,7.73,7.73,0,0,1,10,14.46a8.34,8.34,0,0,1,6-2.32c6.08,0,8.24,4.4,8.24,8.18A14.09,14.09,0,0,1,20.34,30,.75.75,0,0,1,19.77,30.28Z"/><path d="M8.66,27.74a14.14,14.14,0,0,1-1.56-.09.76.76,0,1,1,.17-1.52c2.49.28,4.45-.16,5.84-1.32a6.37,6.37,0,0,0,2.12-4.53.75.75,0,0,1,.82-.71.78.78,0,0,1,.72.81A7.89,7.89,0,0,1,14.09,26,8.2,8.2,0,0,1,8.66,27.74Z"/></svg>',
|
||||||
logout: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M6,30H18a2.0023,2.0023,0,0,0,2-2V25H18v3H6V4H18V7h2V4a2.0023,2.0023,0,0,0-2-2H6A2.0023,2.0023,0,0,0,4,4V28A2.0023,2.0023,0,0,0,6,30Z"/><path d="M20.586 20.586L24.172 17 10 17 10 15 24.172 15 20.586 11.414 22 10 28 16 22 22 20.586 20.586z"/></svg>',
|
logout: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M6,30H18a2.0023,2.0023,0,0,0,2-2V25H18v3H6V4H18V7h2V4a2.0023,2.0023,0,0,0-2-2H6A2.0023,2.0023,0,0,0,4,4V28A2.0023,2.0023,0,0,0,6,30Z"/><path d="M20.586 20.586L24.172 17 10 17 10 15 24.172 15 20.586 11.414 22 10 28 16 22 22 20.586 20.586z"/></svg>',
|
||||||
|
login: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M26,30H14a2,2,0,0,1-2-2V25h2v3H26V4H14V7H12V4a2,2,0,0,1,2-2H26a2,2,0,0,1,2,2V28A2,2,0,0,1,26,30Z"/><path d="M14.59 20.59L18.17 17 4 17 4 15 18.17 15 14.59 11.41 16 10 22 16 16 22 14.59 20.59z"/></svg>',
|
||||||
|
help: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M16,2A14,14,0,1,0,30,16,14,14,0,0,0,16,2Zm0,26A12,12,0,1,1,28,16,12,12,0,0,1,16,28Z"/><path d="M17,8H15.5A4.49,4.49,0,0,0,11,12.5V13h2v-.5A2.5,2.5,0,0,1,15.5,10H17a2.5,2.5,0,0,1,0,5H15v4.5h2V17a4.5,4.5,0,0,0,0-9Z"/></svg>',
|
||||||
|
home: '<svg slot="title-icon" width="16" height="16" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M16.6123,2.2138a1.01,1.01,0,0,0-1.2427,0L1,13.4194l1.2427,1.5717L4,13.6209V26a2.0041,2.0041,0,0,0,2,2H26a2.0037,2.0037,0,0,0,2-2V13.63L29.7573,15,31,13.4282ZM18,26H14V18h4Zm2,0V18a2.0023,2.0023,0,0,0-2-2H14a2.002,2.002,0,0,0-2,2v8H6V12.0615l10-7.79,10,7.8005V26Z"/></svg>',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing-page body: the tool tiles when signed in, a sign-in call to action
|
||||||
|
* when not. Both are injected server-side into index.html's `<!--landing-view-->`
|
||||||
|
* marker so the two states never share markup or flash the wrong one.
|
||||||
|
*/
|
||||||
|
const LANDING_TOOLS = `
|
||||||
|
<div class="tool-grid">
|
||||||
|
<cds-clickable-tile href="/cropper">
|
||||||
|
<div class="tool-card">
|
||||||
|
<span class="tool-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2v14a2 2 0 0 0 2 2h14"/><path d="M18 22V8a2 2 0 0 0-2-2H2"/></svg>
|
||||||
|
</span>
|
||||||
|
<h3>Image Cropper</h3>
|
||||||
|
<p>Review, crop and save identity photos submitted for YOUreka ID domain profiles.</p>
|
||||||
|
<span class="go">Open Image Cropper
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</cds-clickable-tile>
|
||||||
|
|
||||||
|
<cds-clickable-tile href="/fingerprint">
|
||||||
|
<div class="tool-card">
|
||||||
|
<span class="tool-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a8 8 0 0 0-8 8c0 3.5 1 5.5 2 7"/><path d="M16 21a10 10 0 0 0 4-13 8 8 0 0 0-15.7-1"/><path d="M8 21a8 8 0 0 1-1-11 5 5 0 0 1 9.7 1.4A6 6 0 0 1 12 17"/><path d="M12 21a4 4 0 0 0 4-4 4 4 0 0 0-1.2-2.9"/></svg>
|
||||||
|
</span>
|
||||||
|
<h3>Fingerprint Agent</h3>
|
||||||
|
<p>Enroll staff fingerprints with the scanner attached to this laptop. Captures are authorized per domain.</p>
|
||||||
|
<span class="go">Open Fingerprint Agent
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</cds-clickable-tile>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const LANDING_SIGNIN = `
|
||||||
|
<div class="card message-card narrow">
|
||||||
|
<div class="message-icon info">${ICONS.lock}</div>
|
||||||
|
<h1>Sign in required</h1>
|
||||||
|
<p class="muted">Sign in with your YOUreka account to open the Image Cropper and Fingerprint Agent.</p>
|
||||||
|
<a class="btn btn-primary" href="/auth/login">Sign in with YOUreka<svg width="16" height="16" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M26,30H14a2,2,0,0,1-2-2V25h2v3H26V4H14V7H12V4a2,2,0,0,1,2-2H26a2,2,0,0,1,2,2V28A2,2,0,0,1,26,30Z"/><path d="M14.59 20.59L18.17 17 4 17 4 15 18.17 15 14.59 11.41 16 10 22 16 16 22 14.59 20.59z"/></svg></a>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
// ── app factory ──────────────────────────────────────────────────────────────
|
// ── app factory ──────────────────────────────────────────────────────────────
|
||||||
export function createApp(overrides = {}) {
|
export function createApp(overrides = {}) {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
@@ -237,7 +302,19 @@ export function createApp(overrides = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── static assets (no session work per asset) ─────────────────────────────
|
// ── static assets (no session work per asset) ─────────────────────────────
|
||||||
app.use(express.static(path.join(__dirname, 'public'), { index: false })); // assets only; '/' is session-aware
|
// Versioned asset URLs are safe to cache hard; the .html templates that also
|
||||||
|
// live under /assets must never be cached (they are read server-side anyway).
|
||||||
|
app.use(
|
||||||
|
express.static(path.join(__dirname, 'public'), {
|
||||||
|
index: false,
|
||||||
|
setHeaders: (res, filePath) => {
|
||||||
|
res.setHeader(
|
||||||
|
'Cache-Control',
|
||||||
|
filePath.endsWith('.html') ? 'no-store' : 'public, max-age=31536000, immutable'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
); // assets only; '/' is session-aware
|
||||||
|
|
||||||
// ── session middleware: recover / refresh the YOUreka Auth session ────────
|
// ── session middleware: recover / refresh the YOUreka Auth session ────────
|
||||||
app.use(async (req, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
@@ -324,7 +401,7 @@ export function createApp(overrides = {}) {
|
|||||||
lastName: data.lastName ?? '',
|
lastName: data.lastName ?? '',
|
||||||
phone: data.phone ?? '',
|
phone: data.phone ?? '',
|
||||||
});
|
});
|
||||||
res.redirect('/fingerprint');
|
res.redirect('/');
|
||||||
} catch {
|
} catch {
|
||||||
errorPage(res, 401, 'Login failed', 'Could not reach the YOUreka portal. Please try again.');
|
errorPage(res, 401, 'Login failed', 'Could not reach the YOUreka portal. Please try again.');
|
||||||
}
|
}
|
||||||
@@ -387,9 +464,11 @@ export function createApp(overrides = {}) {
|
|||||||
.set('Vary', 'Cookie')
|
.set('Vary', 'Cookie')
|
||||||
.type('html')
|
.type('html')
|
||||||
.send(
|
.send(
|
||||||
|
versionAssets(
|
||||||
html
|
html
|
||||||
.replace('<!--app-header-->', appHeader(session, active))
|
.replace('<!--app-header-->', appHeader(session, active))
|
||||||
.replace('</head>', ` <script>window.__SESSION__ = ${data};</script>\n</head>`)
|
.replace('</head>', ` <script>window.__SESSION__ = ${data};</script>\n</head>`)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -403,10 +482,20 @@ export function createApp(overrides = {}) {
|
|||||||
.set('Cache-Control', 'no-store')
|
.set('Cache-Control', 'no-store')
|
||||||
.set('Vary', 'Cookie')
|
.set('Vary', 'Cookie')
|
||||||
.type('html')
|
.type('html')
|
||||||
.send(html.replace('<!--app-header-->', appHeader(req.session, null)));
|
.send(
|
||||||
|
versionAssets(
|
||||||
|
html
|
||||||
|
.replace('<!--app-header-->', appHeader(req.session, 'home'))
|
||||||
|
.replace('<!--landing-view-->', req.session ? LANDING_TOOLS : LANDING_SIGNIN)
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/help', (req, res) => {
|
||||||
|
renderPage(res, 'help.html', req.session, 'help');
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/auth/session', requireApi, async (req, res, next) => {
|
app.get('/auth/session', requireApi, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { codes, checkedAt } = await domainContext(req);
|
const { codes, checkedAt } = await domainContext(req);
|
||||||
|
|||||||
+84
-21
@@ -112,7 +112,7 @@ async function login(base) {
|
|||||||
assert.equal(loginRes.status, 302);
|
assert.equal(loginRes.status, 302);
|
||||||
const callbackRes = await fetch(`${base}/auth/callback?sessionId=s1`, manual);
|
const callbackRes = await fetch(`${base}/auth/callback?sessionId=s1`, manual);
|
||||||
assert.equal(callbackRes.status, 302);
|
assert.equal(callbackRes.status, 302);
|
||||||
assert.equal(callbackRes.headers.get('location'), '/fingerprint');
|
assert.equal(callbackRes.headers.get('location'), '/');
|
||||||
const session = cookieValue(callbackRes, 'session');
|
const session = cookieValue(callbackRes, 'session');
|
||||||
const profile = cookieValue(callbackRes, 'user_profile');
|
const profile = cookieValue(callbackRes, 'user_profile');
|
||||||
assert.ok(session, 'session cookie set');
|
assert.ok(session, 'session cookie set');
|
||||||
@@ -121,14 +121,19 @@ async function login(base) {
|
|||||||
return `session=${session}; user_profile=${profile}`;
|
return `session=${session}; user_profile=${profile}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
test('landing page links both tools', async () => {
|
test('signed-out landing prompts sign-in; signed-in landing links both tools', async () => {
|
||||||
const { base, stop } = await bootApp();
|
const { base, stop } = await bootApp();
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/`);
|
const anon = await (await fetch(`${base}/`)).text();
|
||||||
const html = await res.text();
|
// no tools until authenticated — a call to action instead
|
||||||
assert.equal(res.status, 200);
|
assert.match(anon, /href="\/auth\/login">Sign in with YOUreka</);
|
||||||
assert.match(html, /href="\/cropper"/);
|
assert.ok(!anon.includes('href="/cropper"'), 'no cropper tile when signed out');
|
||||||
assert.match(html, /href="\/fingerprint"/);
|
assert.ok(!anon.includes('href="/fingerprint"'), 'no fingerprint tile when signed out');
|
||||||
|
|
||||||
|
const cookies = await login(base);
|
||||||
|
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();
|
||||||
|
assert.match(authed, /href="\/cropper"/);
|
||||||
|
assert.match(authed, /href="\/fingerprint"/);
|
||||||
} finally {
|
} finally {
|
||||||
await stop();
|
await stop();
|
||||||
}
|
}
|
||||||
@@ -350,28 +355,86 @@ test('landing header is server-rendered for the session', async () => {
|
|||||||
const anon = await (await fetch(`${base}/`)).text();
|
const anon = await (await fetch(`${base}/`)).text();
|
||||||
assert.match(anon, /href="\/auth\/login">Sign in</);
|
assert.match(anon, /href="\/auth\/login">Sign in</);
|
||||||
assert.ok(!anon.includes('<!--app-header-->'), 'header marker is replaced');
|
assert.ok(!anon.includes('<!--app-header-->'), 'header marker is replaced');
|
||||||
// tools live in the header nav and in the mobile side nav
|
// signed out: no service links, but Help and Sign in are always there
|
||||||
assert.match(anon, /cds-header-nav-item href="\/cropper"/);
|
assert.ok(!anon.includes('cds-header-nav-item href="/cropper"'), 'no cropper in header nav when signed out');
|
||||||
assert.match(anon, /cds-header-nav-item href="\/fingerprint"/);
|
assert.ok(!anon.includes('cds-header-nav-item href="/fingerprint"'), 'no fingerprint in header nav when signed out');
|
||||||
assert.match(anon, /cds-side-nav-link href="\/cropper"/);
|
assert.match(anon, /cds-header-nav-item href="\/help"/);
|
||||||
assert.match(anon, /cds-side-nav-divider/);
|
assert.match(anon, /cds-side-nav-link href="\/help"/);
|
||||||
// side nav items carry Carbon icons in the title-icon slot
|
// the divider only groups the services, so it is absent when signed out
|
||||||
assert.match(anon, /cds-side-nav-link href="\/cropper"[^>]*><svg slot="title-icon"/);
|
assert.ok(!anon.includes('cds-side-nav-divider'), 'no side nav divider without services');
|
||||||
assert.match(anon, /cds-side-nav-link href="\/fingerprint"[^>]*><svg slot="title-icon"/);
|
assert.ok(!anon.includes('header-nav-divider'), 'no header divider without services');
|
||||||
assert.match(anon, /cds-side-nav-link href="\/auth\/logout"[^>]*><svg slot="title-icon"/);
|
// no identity or sign-out when signed out
|
||||||
assert.match(anon, /cds-side-nav-link href="\/auth\/logout"/);
|
assert.ok(!anon.includes('user-chip'), 'no user chip when signed out');
|
||||||
|
assert.ok(!anon.includes('data-signout'), 'no sign-out when signed out');
|
||||||
|
assert.ok(!anon.includes('signout-modal'), 'no sign-out modal when signed out');
|
||||||
assert.match(anon, /youreka-logo\.png/);
|
assert.match(anon, /youreka-logo\.png/);
|
||||||
assert.match(anon, /header-menu-button/);
|
assert.match(anon, /header-menu-button/);
|
||||||
// favicon is the Youreka logo; sign-out is confirmed via a Carbon modal
|
// the side nav brand is just the logo — no repeated "YOUreka Tools" text
|
||||||
|
assert.match(anon, /<div class="side-nav-brand">\s*<img[^>]*>\s*<\/div>/);
|
||||||
|
// favicon is the Youreka logo
|
||||||
assert.match(anon, /rel="icon"[^>]*youreka-favicon\.png/);
|
assert.match(anon, /rel="icon"[^>]*youreka-favicon\.png/);
|
||||||
assert.match(anon, /data-signout/);
|
|
||||||
assert.match(anon, /cds-modal id="signout-modal"/);
|
|
||||||
assert.match(anon, /id="signout-confirm"/);
|
|
||||||
|
|
||||||
const cookies = await login(base);
|
const cookies = await login(base);
|
||||||
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();
|
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();
|
||||||
assert.match(authed, /Ada/);
|
assert.match(authed, /Ada/);
|
||||||
|
assert.match(authed, /user-chip/);
|
||||||
assert.match(authed, /href="\/auth\/logout"[^>]*>Sign out</);
|
assert.match(authed, /href="\/auth\/logout"[^>]*>Sign out</);
|
||||||
|
// signed in: services live in the header nav and in the mobile side nav
|
||||||
|
assert.match(authed, /cds-header-nav-item href="\/cropper"/);
|
||||||
|
assert.match(authed, /cds-header-nav-item href="\/fingerprint"/);
|
||||||
|
assert.match(authed, /cds-header-nav-item href="\/help"/);
|
||||||
|
assert.match(authed, /cds-header-nav-item href="\/auth\/logout" data-signout/);
|
||||||
|
assert.match(authed, /cds-side-nav-link href="\/cropper"/);
|
||||||
|
assert.match(authed, /cds-side-nav-link href="\/help"/);
|
||||||
|
assert.match(authed, /cds-side-nav-divider/);
|
||||||
|
assert.match(authed, /header-nav-divider/);
|
||||||
|
// side nav items carry Carbon icons in the title-icon slot
|
||||||
|
assert.match(authed, /cds-side-nav-link href="\/cropper"[^>]*><svg slot="title-icon"/);
|
||||||
|
assert.match(authed, /cds-side-nav-link href="\/fingerprint"[^>]*><svg slot="title-icon"/);
|
||||||
|
assert.match(authed, /cds-side-nav-link href="\/help"[^>]*><svg slot="title-icon"/);
|
||||||
|
assert.match(authed, /cds-side-nav-link href="\/auth\/logout"[^>]*><svg slot="title-icon"/);
|
||||||
|
// sign-out is confirmed via a Carbon modal
|
||||||
|
assert.match(authed, /data-signout/);
|
||||||
|
assert.match(authed, /cds-modal id="signout-modal"/);
|
||||||
|
assert.match(authed, /id="signout-confirm"/);
|
||||||
|
} finally {
|
||||||
|
await stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('help page is public and documents each service', async () => {
|
||||||
|
const { base, stop } = await bootApp();
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/help`);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const html = await res.text();
|
||||||
|
assert.ok(!html.includes('<!--app-header-->'), 'header marker is replaced');
|
||||||
|
assert.match(html, /cds-accordion-item title="Image Cropper"/);
|
||||||
|
assert.match(html, /cds-accordion-item title="Fingerprint Agent"/);
|
||||||
|
assert.match(html, /accordion\.min\.js/);
|
||||||
|
assert.match(html, /cds-header-nav-item href="\/help" aria-current="page"/);
|
||||||
|
assert.match(html, /cds-side-nav-link href="\/help" aria-current="page"/);
|
||||||
|
} finally {
|
||||||
|
await stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('asset URLs are versioned per deployment; HTML is never cached', async () => {
|
||||||
|
const { base, stop } = await bootApp();
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/`);
|
||||||
|
const html = await res.text();
|
||||||
|
assert.equal(res.headers.get('cache-control'), 'no-store');
|
||||||
|
assert.match(html, /\/assets\/style\.css\?v=[a-z0-9]+/);
|
||||||
|
assert.match(html, /\/assets\/youreka-favicon\.png\?v=[a-z0-9]+/);
|
||||||
|
|
||||||
|
// versioned assets may be cached hard...
|
||||||
|
const css = await fetch(`${base}/assets/style.css?v=x`);
|
||||||
|
assert.equal(css.status, 200);
|
||||||
|
assert.match(css.headers.get('cache-control'), /immutable/);
|
||||||
|
// ...but the raw .html templates must not be
|
||||||
|
const tpl = await fetch(`${base}/assets/help.html`);
|
||||||
|
assert.equal(tpl.headers.get('cache-control'), 'no-store');
|
||||||
} finally {
|
} finally {
|
||||||
await stop();
|
await stop();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user