HIGH
- async handlers: Express 4 does not catch rejected promises and Node 22
exits on unhandled rejections — a malformed body ({} to /cropper/dequeue
etc.) could hang a request or kill the process. Every cropper route is
now wrapped (throw → 500 via next), request bodies are validated up
front (400), and JSON.parse of queue items is guarded. Server-wide
error middleware added, honouring err.status (413 for oversized bodies).
- error pages HTML-escape all interpolated values — the login failure page
rendered upstream (portal) error text raw.
MEDIUM
- outbound calls carry AbortSignal timeouts (5s OpenFGA verify, 15s
cropper upstreams) — a hung upstream can no longer pin a request.
- body limits scoped: /cropper gets 50mb (picture data URLs), everything
else 1mb (previously 50mb globally, memory-DoS surface pre-auth).
- cropper save validates the client-supplied domain against the catalog;
picture fetches validate the Minio object key (id-domains/ prefix, no ..).
LOW
- static assets served before the session middleware (no PocketBase load
per asset); landing header builds DOM nodes with textContent instead of
innerHTML; picture load checks upstream ok.
5 new tests (40 total) covering malformed bodies, malformed queue items,
unknown domains, object-key traversal, and error-page escaping.
99 lines
5.3 KiB
HTML
99 lines
5.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>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" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' rx='4' fill='%230f62fe'/><path d='M12 5a6 6 0 0 0-6 6c0 2.6.8 4.1 1.5 5.2' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M15.5 19a7.5 7.5 0 0 0 3-9.7A6 6 0 0 0 7.1 7.9' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M9.5 18.5a5.5 5.5 0 0 1-.8-7.6 3.8 3.8 0 0 1 7.2 1.5 4.3 4.3 0 0 1-3.4 4.6' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/></svg>">
|
|
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/tile.min.js"></script>
|
|
<style>
|
|
cds-tile:not(:defined), cds-clickable-tile:not(:defined) { visibility: hidden; }
|
|
</style>
|
|
</head>
|
|
<body class="app">
|
|
<nav class="nav">
|
|
<div class="container">
|
|
<a class="brand" href="/">
|
|
<span class="brand-mark">
|
|
<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>
|
|
YOUreka <small>Tools</small>
|
|
</a>
|
|
<div class="nav-right" id="nav-session">
|
|
<a class="linklike" href="/auth/login">Sign in</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="container">
|
|
<div class="hero">
|
|
<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>
|
|
</div>
|
|
|
|
<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>
|
|
</main>
|
|
|
|
<footer class="foot">
|
|
<div class="container">
|
|
<span>YOUreka IDID · Fingerprint Service</span>
|
|
<span>Scan captures run locally via the fpagent agent — no scanner data leaves this machine except the enrolled artifacts you save.</span>
|
|
</div>
|
|
</footer>
|
|
<script>
|
|
// Reflect the current session in the header: Sign in vs user chip + Sign out.
|
|
fetch('/auth/session')
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (session) {
|
|
var host = document.getElementById('nav-session');
|
|
if (!session) return; // keep the Sign in link
|
|
var name = (session.firstName || session.phone || '?').trim();
|
|
host.textContent = '';
|
|
var chip = document.createElement('span');
|
|
chip.className = 'user-chip';
|
|
var avatar = document.createElement('span');
|
|
avatar.className = 'avatar';
|
|
avatar.textContent = name.slice(0, 2).toUpperCase(); // textContent, never innerHTML
|
|
var label = document.createElement('span');
|
|
label.textContent = name;
|
|
chip.append(avatar, label);
|
|
var signOut = document.createElement('a');
|
|
signOut.className = 'linklike';
|
|
signOut.href = '/auth/logout';
|
|
signOut.textContent = 'Sign out';
|
|
host.append(chip, signOut);
|
|
})
|
|
.catch(function () { /* keep Sign in */ });
|
|
</script>
|
|
</body>
|
|
</html>
|