Improving Win95 styling
This commit is contained in:
Vendored
+236
@@ -0,0 +1,236 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
function envFlag(name, fallback) {
|
||||
const value = process.env[name];
|
||||
if (value === undefined)
|
||||
return fallback;
|
||||
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
|
||||
}
|
||||
function trimTrailingSlash(value) {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
export function labAuthConfig() {
|
||||
const gnommowebUrl = trimTrailingSlash(process.env.GNOMMOWEB_URL ?? 'https://glitch.university');
|
||||
return {
|
||||
enabled: envFlag('LAB_AUTH_ENABLED', process.env.NODE_ENV === 'production'),
|
||||
gnommowebUrl,
|
||||
jwtSecret: process.env.JWT_SECRET ?? '',
|
||||
keycardMeritSlug: process.env.LAB_KEYCARD_MERIT_SLUG ?? 'lab-keycard',
|
||||
keycardRequired: envFlag('LAB_KEYCARD_REQUIRED', true),
|
||||
signInUrl: process.env.LAB_SIGN_IN_URL ?? `${gnommowebUrl}/auth/google`,
|
||||
userProfileUrl: process.env.LAB_USER_PROFILE_URL ?? `${gnommowebUrl}/api/user/profile`
|
||||
};
|
||||
}
|
||||
function wantsHtml(request) {
|
||||
const accept = request.get('accept') ?? '';
|
||||
return accept.includes('text/html') || accept.includes('*/*');
|
||||
}
|
||||
function currentUrl(request) {
|
||||
const proto = request.get('x-forwarded-proto') ?? request.protocol;
|
||||
const host = request.get('x-forwarded-host') ?? request.get('host') ?? 'localhost';
|
||||
return `${proto}://${host}${request.originalUrl}`;
|
||||
}
|
||||
function loginUrl(request, config) {
|
||||
const returnTo = currentUrl(request);
|
||||
if (config.signInUrl.includes('{returnTo}')) {
|
||||
return config.signInUrl.replace('{returnTo}', encodeURIComponent(returnTo));
|
||||
}
|
||||
const url = new URL(config.signInUrl);
|
||||
url.searchParams.set('returnTo', returnTo);
|
||||
return url.toString();
|
||||
}
|
||||
function lockedPage(access, config) {
|
||||
const unlockUrl = access.unlockUrl ?? `${config.gnommowebUrl}/tech-tree`;
|
||||
const reason = access.reason ?? 'Your Glitch University account does not have the CA Lab Keycard yet.';
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CA Lab Locked</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #008080;
|
||||
color: #000;
|
||||
font-family: "MS Sans Serif", Tahoma, Arial, sans-serif;
|
||||
}
|
||||
main {
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
border: 2px solid;
|
||||
border-color: #fff #404040 #404040 #fff;
|
||||
background: #c0c0c0;
|
||||
box-shadow: 4px 4px 0 rgb(0 0 0 / 0.35);
|
||||
}
|
||||
header {
|
||||
padding: 6px 8px;
|
||||
background: linear-gradient(90deg, #000080, #1084d0);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
section { padding: 18px; }
|
||||
h1 { margin: 0 0 10px; font-size: 1.2rem; }
|
||||
p { line-height: 1.45; }
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
margin-top: 10px;
|
||||
padding: 0 14px;
|
||||
border: 2px solid;
|
||||
border-color: #fff #404040 #404040 #fff;
|
||||
background: #c0c0c0;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>CA Lab Computer</header>
|
||||
<section>
|
||||
<h1>Lab keycard required</h1>
|
||||
<p>${escapeHtml(reason)}</p>
|
||||
<p>Earn the Cellular Automata lab merit on glitch.university, then return here.</p>
|
||||
<a href="${escapeHtml(unlockUrl)}">Go to Glitch University</a>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
function isMeritSlug(value, requiredSlug) {
|
||||
return typeof value === 'string' && value.trim() === requiredSlug;
|
||||
}
|
||||
function collectionHasMeritSlug(value, requiredSlug) {
|
||||
if (!Array.isArray(value))
|
||||
return false;
|
||||
return value.some((entry) => {
|
||||
if (isMeritSlug(entry, requiredSlug))
|
||||
return true;
|
||||
if (!entry || typeof entry !== 'object')
|
||||
return false;
|
||||
const record = entry;
|
||||
return isMeritSlug(record.slug, requiredSlug);
|
||||
});
|
||||
}
|
||||
function payloadHasMeritSlug(payload, requiredSlug) {
|
||||
if (!payload || typeof payload !== 'object')
|
||||
return false;
|
||||
const record = payload;
|
||||
const user = record.user && typeof record.user === 'object'
|
||||
? record.user
|
||||
: undefined;
|
||||
return (isMeritSlug(record.merit_slug, requiredSlug) ||
|
||||
collectionHasMeritSlug(record.merit_slugs, requiredSlug) ||
|
||||
collectionHasMeritSlug(record.merits, requiredSlug) ||
|
||||
(user ? collectionHasMeritSlug(user.merits, requiredSlug) : false));
|
||||
}
|
||||
function jwtClaimsHaveKeycard(user, config) {
|
||||
if (!user)
|
||||
return false;
|
||||
return isMeritSlug(user.merit_slug, config.keycardMeritSlug) ||
|
||||
collectionHasMeritSlug(user.merit_slugs, config.keycardMeritSlug);
|
||||
}
|
||||
async function checkUserProfileForKeycard(token, config) {
|
||||
const response = await fetch(config.userProfileUrl, {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
cookie: `auth_token=${encodeURIComponent(token)}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: response.status === 401
|
||||
? 'Sign in to glitch.university to access the CA lab.'
|
||||
: `The CA Lab Keycard could not be verified. The user profile endpoint returned HTTP ${response.status}.`
|
||||
};
|
||||
}
|
||||
const profile = await response.json();
|
||||
if (payloadHasMeritSlug(profile, config.keycardMeritSlug)) {
|
||||
return { allowed: true, reason: 'CA Lab Keycard found in gnommoweb user profile.' };
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Your Glitch University account does not have the CA Lab Keycard yet.'
|
||||
};
|
||||
}
|
||||
export function requireLabAccess(config = labAuthConfig()) {
|
||||
return async (request, response, next) => {
|
||||
if (!config.enabled) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (!config.jwtSecret) {
|
||||
response.status(500).json({ error: 'Lab authentication is enabled but JWT_SECRET is not configured' });
|
||||
return;
|
||||
}
|
||||
const token = request.cookies?.auth_token;
|
||||
if (!token) {
|
||||
if (wantsHtml(request)) {
|
||||
response.redirect(loginUrl(request, config));
|
||||
return;
|
||||
}
|
||||
response.status(401).json({
|
||||
error: 'Authentication required',
|
||||
signInUrl: loginUrl(request, config)
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
request.labUser = jwt.verify(token, config.jwtSecret);
|
||||
}
|
||||
catch {
|
||||
if (wantsHtml(request)) {
|
||||
response.redirect(loginUrl(request, config));
|
||||
return;
|
||||
}
|
||||
response.status(401).json({
|
||||
error: 'Invalid or expired token',
|
||||
signInUrl: loginUrl(request, config)
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!config.keycardRequired) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (jwtClaimsHaveKeycard(request.labUser, config)) {
|
||||
request.labAccess = { allowed: true, reason: 'CA Lab Keycard found in JWT claims.' };
|
||||
next();
|
||||
return;
|
||||
}
|
||||
let access;
|
||||
try {
|
||||
access = await checkUserProfileForKeycard(token, config);
|
||||
}
|
||||
catch {
|
||||
access = { allowed: false, reason: 'The CA Lab Keycard service is unavailable.' };
|
||||
}
|
||||
request.labAccess = access;
|
||||
if (access.allowed === true) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (wantsHtml(request)) {
|
||||
response.status(403).type('html').send(lockedPage(access, config));
|
||||
return;
|
||||
}
|
||||
response.status(403).json({
|
||||
error: 'CA Lab Keycard required',
|
||||
reason: access.reason,
|
||||
unlockUrl: access.unlockUrl ?? `${config.gnommowebUrl}/tech-tree`
|
||||
});
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=labAuth.js.map
|
||||
Reference in New Issue
Block a user