167 lines
7.0 KiB
HTML
167 lines
7.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Paskia - Dev Mode</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark; /* Automatic themes by browser */
|
|
}
|
|
/* Login/reauth/forbidden dialog will appear in this iframe */
|
|
#auth-iframe {
|
|
/* Full viewport overlay */
|
|
border: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 9999;
|
|
/* Optional transparent background with optional blur backdrop */
|
|
color-scheme: auto;
|
|
background: transparent;
|
|
backdrop-filter: blur(.1rem) brightness(0.7);
|
|
-webkit-backdrop-filter: blur(.1rem) brightness(0.7);
|
|
}
|
|
/* Prevent background scroll when auth-iframe is shown */
|
|
body:has(#auth-iframe) {
|
|
overflow: hidden;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>🔐 Paskia - Development Server</h1>
|
|
<p class="subtitle">The following features are available after you have registered your Admin account and logged in. You should also use the Admin Site to create non-privileged users to see the Forbidden dialog caused by missing permissions.</p>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<div class="section">
|
|
<h2>Management Site</h2>
|
|
<button onclick="window.open('/auth/', '_blank')">👤 User Profile</button>
|
|
<button onclick="window.open('/auth/admin/', '_blank')">⚙️ Admin Panel</button>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>API Mode (not leaving the page)</h2>
|
|
<p>For SPAs and fetch() calls - shows auth in an iframe overlay:</p>
|
|
<button onclick="apiCall('/auth/api/user-info', 'POST')">📋 Get User Info</button>
|
|
<button onclick="apiCall('/auth/api/forward?max_age=10s')">🔄 Reauth (max_age=10s)</button>
|
|
<button onclick="apiCall('/auth/api/forward?perm=auth:admin')">🛡️ Admin Only</button>
|
|
<button onclick="logout()">🚪 Logout</button>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Browser Mode (full page)</h2>
|
|
<p>Block access to otherwise open site - intended for forward-auth mechanism (Caddy, Nginx):</p>
|
|
<button onclick="browserNav('/auth/api/forward')">🔐 Basic Auth</button>
|
|
<button onclick="browserNav('/auth/api/forward?max_age=10s')">🔄 Reauth (max_age=10s)</button>
|
|
<button onclick="browserNav('/auth/api/forward?perm=auth:admin')">🛡️ Admin Only</button>
|
|
</div>
|
|
|
|
<pre id="output">Click a button to test...</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const output = document.getElementById('output');
|
|
let pendingCall = null; // Stores the API call to retry after auth
|
|
|
|
// The auth iframe posts messages when authentication completes or is cancelled.
|
|
// Message types: 'auth-success' (proceed), 'auth-back' (user cancelled)
|
|
// Errors during auth stay in the dialog allowing retry, no message is sent.
|
|
window.addEventListener('message', (event) => {
|
|
const { type, message } = event.data || {};
|
|
|
|
if (type === 'auth-success') {
|
|
log('✓ Authentication successful, retrying...');
|
|
hideAuthIframe();
|
|
// Retry the original API call that triggered authentication
|
|
if (pendingCall) {
|
|
const { url, method } = pendingCall;
|
|
pendingCall = null;
|
|
apiCall(url, method);
|
|
}
|
|
} else if (type === 'auth-back') {
|
|
log(message || 'Authentication cancelled');
|
|
hideAuthIframe();
|
|
pendingCall = null;
|
|
}
|
|
});
|
|
|
|
// Make an API call, handling 401/403 by showing the auth iframe.
|
|
// The server returns JSON with auth.iframe URL when authentication is needed.
|
|
async function apiCall(url, method = 'GET') {
|
|
log(`${method} ${url}...`);
|
|
|
|
const response = await fetch(url, { method, credentials: 'include' });
|
|
|
|
// Server returns 401 (login/reauth) or 403 (missing permissions)
|
|
// with a JSON body containing the iframe URL for authentication
|
|
if (response.status === 401 || response.status === 403) {
|
|
const data = await response.json();
|
|
if (data.auth?.iframe) {
|
|
const mode = data.auth.mode; // 'login' or 'reauth'
|
|
log(`${mode === 'reauth' ? 'Re-authentication' : 'Authentication'} required...`);
|
|
pendingCall = { url, method };
|
|
showAuthIframe(data.auth.iframe);
|
|
return;
|
|
}
|
|
log(`Error: ${response.status} - ${data.detail}`);
|
|
return;
|
|
}
|
|
|
|
// Forward endpoint returns 204 on success (Caddy then adds Remote-* headers)
|
|
if (response.status === 204) {
|
|
log('✓ Success (204 No Content)\nHeaders:\n' +
|
|
[...response.headers].filter(([k]) => k.startsWith('remote-'))
|
|
.map(([k, v]) => ` ${k}: ${v}`).join('\n'));
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
log(`Error: ${response.status} ${response.statusText}`);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
log('✓ Response:\n' + JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
async function logout() {
|
|
await fetch('/auth/api/logout', { method: 'POST', credentials: 'include' });
|
|
log('Logged out');
|
|
}
|
|
|
|
// Create fullscreen iframe for authentication.
|
|
// The 'allow' attribute enables WebAuthn (passkey) API inside the iframe.
|
|
function showAuthIframe(url) {
|
|
hideAuthIframe();
|
|
const iframe = document.createElement('iframe');
|
|
iframe.id = 'auth-iframe';
|
|
iframe.src = url;
|
|
document.body.appendChild(iframe);
|
|
log("Authentication dialog open...")
|
|
}
|
|
|
|
function hideAuthIframe() {
|
|
document.getElementById('auth-iframe')?.remove();
|
|
}
|
|
|
|
function log(msg) {
|
|
output.textContent = msg;
|
|
}
|
|
|
|
// Browser mode: open the forward endpoint directly in a new window.
|
|
// When Accept: text/html, the server redirects to the login page if needed,
|
|
// then back to the original URL after authentication.
|
|
function browserNav(url) {
|
|
log('Opening in new window...\nIf not authenticated, you\'ll see the login page.\nAfter auth, you\'ll see a 204 response (blank page = success).');
|
|
window.open(url, '_blank');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|