${ICONS[icon] || ICONS.alert}
@@ -118,6 +106,50 @@ function esc(value) {
.replace(/'/g, ''');
};
+/**
+ * The application header and mobile side nav (Carbon ui-shell), shared by
+ * every page. `session` is present when the operator is signed in; `active`
+ * marks the current tool. Rendered server-side so pages need no client
+ * session fetch and hold a single header definition.
+ */
+const appHeader = (session, active) => {
+ const toolLink = (href, label, key) =>
+ `
${label}`;
+ const sideLink = (href, label, key) =>
+ `
${label}`;
+ const name = session ? (session.firstName || session.phone || '') : '';
+ const right = session
+ ? `
${esc(name.trim().slice(0, 2).toUpperCase())}${esc(name)}
+
Sign out`
+ : '
Sign in';
+ return `
+
+
+
+
YOUreka Tools
+
+
+ ${toolLink('/cropper', 'Image Cropper', 'cropper')}
+ ${toolLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
+
+ ${active === 'fingerprint' ? '' : ''}
+
+
+
+
+

+
YOUreka Tools
+
+
+ ${sideLink('/cropper', 'Image Cropper', 'cropper')}
+ ${sideLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
+
+ Sign out
+
+
+`;
+};
+
// ── app factory ──────────────────────────────────────────────────────────────
export function createApp(overrides = {}) {
const cfg = {
@@ -300,7 +332,7 @@ export function createApp(overrides = {}) {
* client never has to fetch it (and never flashes a loading state).
* Per-user content — must not be cached.
*/
- const renderPage = (res, file, session) => {
+ const renderPage = (res, file, session, active) => {
fs.readFile(path.join(__dirname, 'public', 'assets', file), 'utf8', (error, html) => {
if (error) return errorPage(res, 500, 'Something went wrong', 'The page could not be loaded.');
const data = session
@@ -310,21 +342,24 @@ export function createApp(overrides = {}) {
.set('Cache-Control', 'no-store')
.set('Vary', 'Cookie')
.type('html')
- .send(html.replace('', ` \n`));
+ .send(
+ html
+ .replace('', appHeader(session, active))
+ .replace('', ` \n`)
+ );
});
};
+
+
app.get('/', (req, res, next) => {
fs.readFile(path.join(__dirname, 'public', 'index.html'), 'utf8', (error, html) => {
if (error) return next(error);
- const nav = req.session
- ? `
${esc((req.session.firstName || req.session.phone || '?').trim().slice(0, 2).toUpperCase())}${esc(req.session.firstName || req.session.phone || '')}Sign out`
- : '
Sign in';
res
.set('Cache-Control', 'no-store')
.set('Vary', 'Cookie')
.type('html')
- .send(html.replace('', nav));
+ .send(html.replace('', appHeader(req.session, null)));
});
});
@@ -363,7 +398,7 @@ export function createApp(overrides = {}) {
// ── wizard page (session required) ────────────────────────────────────────
app.get('/fingerprint', requirePage, async (req, res, next) => {
try {
- renderPage(res, 'fp/index.html', await sessionPayload(req, { withDomains: true }));
+ renderPage(res, 'fp/index.html', await sessionPayload(req, { withDomains: true }), 'fingerprint');
} catch (e) {
next(e);
}
@@ -399,11 +434,12 @@ export function createApp(overrides = {}) {
next();
};
app.get('/cropper', requirePage, pageCropGate, (req, res, next) => {
- renderPage(res, 'cropper/index.html', {
- phone: req.session.phone,
- firstName: req.session.firstName,
- lastName: req.session.lastName,
- });
+ renderPage(
+ res,
+ 'cropper/index.html',
+ { phone: req.session.phone, firstName: req.session.firstName, lastName: req.session.lastName },
+ 'cropper'
+ );
});
const cropperClients = cfg.cropperClients !== undefined ? cfg.cropperClients : buildCropperClients();
if (cropperClients) {
diff --git a/tools/test/server.test.js b/tools/test/server.test.js
index 55080b0..1a9f909 100644
--- a/tools/test/server.test.js
+++ b/tools/test/server.test.js
@@ -317,7 +317,15 @@ test('landing header is server-rendered for the session', async () => {
try {
const anon = await (await fetch(`${base}/`)).text();
assert.match(anon, /href="\/auth\/login">Sign in);
- assert.ok(!anon.includes(''), 'marker is replaced');
+ assert.ok(!anon.includes(''), 'header marker is replaced');
+ // tools live in the header nav and in the mobile side nav
+ assert.match(anon, /cds-header-nav-item href="\/cropper"/);
+ assert.match(anon, /cds-header-nav-item href="\/fingerprint"/);
+ assert.match(anon, /cds-side-nav-link href="\/cropper"/);
+ assert.match(anon, /cds-side-nav-divider/);
+ assert.match(anon, /cds-side-nav-link href="\/auth\/logout"/);
+ assert.match(anon, /youreka-logo\.png/);
+ assert.match(anon, /header-menu-button/);
const cookies = await login(base);
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();