fix(cropper): professional permission-denied page

Centered card with navbar, lock icon, sans-serif stack (error pages
previously rendered in the browser default serif), and action-aware
copy: permission denial offers only 'Back to Tools' instead of a
pointless 'Try again'; auth failures keep Try again + Back to Tools.
This commit is contained in:
2026-10-01 01:35:37 +01:00
parent 2f0a1fab21
commit 22d9ba40e3
3 changed files with 31 additions and 9 deletions
+25 -6
View File
@@ -58,7 +58,10 @@ function readProfile(raw) {
}
}
function errorPage(res, status, title, message) {
function errorPage(res, status, title, message, actions = {}) {
const primary = actions.primary || { href: '/auth/login', label: 'Try again' };
const secondary = actions.secondary === null ? null : actions.secondary || { href: '/', label: 'Back to Tools' };
const icon = actions.icon || 'alert';
res
.status(status)
.type('html')
@@ -71,13 +74,26 @@ function errorPage(res, status, title, message) {
<link rel="stylesheet" href="/assets/style.css">
</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">
<a class="linklike" href="/auth/logout">Sign out</a>
</div>
</div>
</nav>
<main class="container narrow center-v">
<div class="card message-card">
<div class="message-icon error">${ICONS.alert}</div>
<div class="message-icon error">${ICONS[icon] || ICONS.alert}</div>
<h1>${title}</h1>
<p class="muted">${message}</p>
<a class="btn btn-primary" href="/auth/login">Try again</a>
<a class="btn btn-ghost" href="/">Back to Tools</a>
<a class="btn btn-primary" href="${primary.href}">${primary.label}</a>
${secondary ? `<a class="btn btn-ghost" href="${secondary.href}">${secondary.label}</a>` : ''}
</div>
</main>
</body>
@@ -87,6 +103,8 @@ function errorPage(res, status, title, message) {
const ICONS = {
alert:
'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
lock:
'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
};
// ── app factory ──────────────────────────────────────────────────────────────
@@ -304,8 +322,9 @@ export function createApp(overrides = {}) {
return errorPage(
res,
403,
'Not permitted',
'You are not permitted to crop images. Contact your administrator if you believe this is a mistake.'
'No access',
'Your account doesn\u2019t have permission to crop images. Ask a domain administrator to grant you access.',
{ icon: 'lock', primary: { href: '/', label: 'Back to Tools' }, secondary: null }
);
}
next();