ba90eff5db
Adds the cyber theme to the console and login page, replaces the dark/light toggle with a theme switcher, and moves the login page script to /login.js so it is served under the existing CSP.
54 lines
14 KiB
TypeScript
54 lines
14 KiB
TypeScript
type LoginPageOptions = {
|
||
mode: 'local' | 'oidc'
|
||
csrf: string
|
||
returnTo: string
|
||
error: string | null
|
||
styleNonce: string
|
||
}
|
||
|
||
export function renderLoginPage(options: LoginPageOptions): string {
|
||
const destination = options.returnTo === '/console/aws' ? '' : `<div class="dest">Landing on <span>${escapeHtml(options.returnTo)}</span></div>`
|
||
const alert = options.error ? `<div class="alert" role="alert">${escapeHtml(options.error)}</div>` : ''
|
||
const signIn = options.mode === 'oidc'
|
||
? `<form method="get" action="/api/auth/oidc/login" data-sso-form><input type="hidden" name="returnTo" value="${escapeHtml(options.returnTo)}"><button class="btn" type="submit"><img class="authentik-icon" src="/authentik.svg" alt="" aria-hidden="true"><span class="sp" aria-hidden="true"></span><span id="label">Sign in with Authentik</span></button></form>`
|
||
: `<form method="post" action="/api/auth/login" class="local-form"><input type="hidden" name="csrf" value="${escapeHtml(options.csrf)}"><input type="hidden" name="returnTo" value="${escapeHtml(options.returnTo)}"><label for="username">Username</label><input id="username" name="username" autocomplete="username" required autofocus><label for="password">Password</label><input id="password" name="password" type="password" autocomplete="current-password" required><button class="btn" type="submit">Sign in</button></form>`
|
||
|
||
return `<!doctype html>
|
||
<html lang="en" data-theme="dark">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark light">
|
||
<link rel="icon" href="/TheCloud.png" />
|
||
<title>Sign in · TheCloud</title>
|
||
<style nonce="${options.styleNonce}">
|
||
:root[data-theme=dark]{--bg:#07111d;--rail:#091727;--panel:#0c1a2b;--fg:#e8f1fa;--dim:#bdd0e3;--mut:#8aa3bc;--ac:#6cc4ff;--acr:108,196,255;--ac2:#ffb547;--bd:rgba(232,241,250,.1);--bd2:rgba(232,241,250,.2);--on:#06223a;--err:#ff9a9a;--errbg:#3a1822}
|
||
:root[data-theme=light]{--bg:#eaf2fa;--rail:#f6faff;--panel:#fff;--fg:#0e2238;--dim:#2f4660;--mut:#4a627a;--ac:#0a6fc2;--acr:10,111,194;--ac2:#c26a00;--bd:rgba(14,34,56,.12);--bd2:rgba(14,34,56,.26);--on:#fff;--err:#a4262c;--errbg:#fdeaea}
|
||
:root[data-theme=cyber]{--bg:#02060a;--rail:#050d10;--panel:#050d10;--fg:#4eff96;--dim:#8affb8;--mut:#2f9a68;--ac:#ffea00;--acr:255,234,0;--ac2:#ff10f0;--bd:rgba(78,255,150,.2);--bd2:rgba(78,255,150,.42);--on:#02060a;--err:#ff8da1;--errbg:#2a0a14}
|
||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;min-width:320px;background:var(--bg);color:var(--fg);font-family:system-ui,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}.mono{font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace}.serif{font-family:ui-serif,"Iowan Old Style","Palatino Linotype",Georgia,serif}.bar{position:fixed;top:0;left:0;height:3px;width:0;z-index:10;background:linear-gradient(90deg,var(--ac),var(--ac2))}.shell{display:grid;grid-template-columns:minmax(310px,24vw) 1fr;min-height:100vh}.rail{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:38px;border-right:1px solid var(--bd);background:var(--rail)}.logo{display:flex;align-items:center;gap:16px;color:var(--ac);text-decoration:none}.brand-mark{width:82px;height:82px;object-fit:contain;filter:drop-shadow(0 8px 16px rgba(0,0,0,.18))}.brand-name{font-size:30px;font-weight:750;letter-spacing:-.06em}.login{max-width:304px;margin:auto 0;transform:translateY(-44px)}.login h1{font-size:17px;margin:0 0 7px}.lead{margin:0;color:var(--mut);font-size:14px;line-height:1.55}.dest,.alert{margin-top:16px;padding:10px 12px;border-radius:6px;font-size:12px;line-height:1.45}.dest{border:1px dashed var(--bd2);color:var(--mut);overflow-wrap:anywhere}.dest span{color:var(--fg)}.alert{border:1px solid var(--err);background:var(--errbg);color:var(--err)}.btn{width:100%;min-height:50px;margin-top:22px;display:flex;align-items:center;justify-content:center;gap:10px;border:0;border-radius:999px;background:var(--ac);color:var(--on);font:700 14px system-ui,sans-serif;cursor:pointer;transition:transform .2s,box-shadow .2s}.btn:hover{transform:translateY(-2px);box-shadow:0 12px 28px rgba(var(--acr),.3)}.btn:focus-visible,.tg button:focus-visible{outline:2px solid var(--ac);outline-offset:3px}.authentik-icon{width:20px;height:20px;object-fit:contain}.sp{display:none;width:14px;height:14px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spin .7s linear infinite}.btn.is-loading .sp{display:block}.btn.is-loading .authentik-icon{display:none}@keyframes spin{to{transform:rotate(360deg)}}.fine{margin:14px 0 0;text-align:center;color:var(--mut);font-size:12px;line-height:1.45}.local-form{display:grid;gap:8px;margin-top:18px}.local-form label{margin-top:5px;color:var(--mut);font:11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em}.local-form input:not([type=hidden]){min-height:42px;padding:0 12px;border:1px solid var(--bd2);border-radius:5px;background:var(--panel);color:var(--fg)}.local-form .btn{margin-top:14px}.rail-bottom{color:var(--mut);font-size:12px;line-height:1.55}.tg{display:flex;width:max-content;gap:3px;margin-bottom:18px;padding:3px;border:1px solid var(--bd2);border-radius:99px}.tg button{display:grid;place-items:center;width:29px;height:29px;border:0;border-radius:50%;background:none;color:var(--mut);cursor:pointer}.tg button.on{background:var(--ac);color:var(--on)}.canvas{min-width:0;padding:20px}.hero{position:relative;min-height:760px;display:grid;place-items:center;overflow:hidden;padding:80px 48px;border:1px solid var(--bd2);border-radius:28px;background:linear-gradient(var(--bd) 1px,transparent 1px),linear-gradient(90deg,var(--bd) 1px,transparent 1px),var(--panel);background-size:56px 56px}.hero::before{content:"";position:absolute;width:720px;height:720px;left:-300px;top:-250px;background:radial-gradient(circle,rgba(var(--acr),.22),transparent 68%);pointer-events:none}.hero::after{content:"";position:absolute;width:600px;height:600px;right:-280px;bottom:-280px;background:radial-gradient(circle,rgba(255,181,71,.16),transparent 68%);pointer-events:none}.grid-glow{position:absolute;width:640px;height:640px;border-radius:50%;background:radial-gradient(circle,rgba(var(--acr),.18),transparent 62%);transform:translate(-50%,-50%);pointer-events:none;opacity:0;transition:opacity .4s}.welcome{position:relative;z-index:1;width:min(100%,1040px);text-align:center;transform:translateY(-24px)}.tag{display:inline-block;margin-bottom:20px;padding:5px 9px;border:1px solid rgba(var(--acr),.45);border-radius:3px;background:rgba(var(--acr),.08);color:var(--ac);font:500 11px ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase}.welcome h2{max-width:800px;min-height:2.25em;margin:0 auto 14px;font-size:clamp(2.35rem,5vw,4.8rem);line-height:1.05;letter-spacing:-.05em}.prompt{color:var(--ac);font-family:ui-monospace,monospace}#tw::after{content:"▌";color:var(--ac);margin-left:5px;animation:blink 1s steps(1) infinite}@keyframes blink{50%{opacity:0}}.sub{margin:0 auto 42px;max-width:40rem;color:var(--dim);font-size:clamp(1.05rem,1.8vw,1.3rem);line-height:1.55}.capabilities{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr));gap:14px}.capability{min-height:158px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:18px 10px;border:1px solid var(--bd2);border-radius:18px;background:rgba(12,26,43,.76);box-shadow:0 20px 50px rgba(0,0,0,.12);transition:transform .2s,border-color .2s}.capability:hover{transform:translateY(-5px);border-color:var(--ac)}.capability img{width:46px;height:46px;object-fit:contain;margin-bottom:15px}.capability strong{font-size:13px;line-height:1.2}.capability small{margin-top:5px;color:var(--mut);font-size:12px}.note{margin:38px 0 0;color:var(--dim);font-size:14px}.quickstart{display:grid;grid-template-columns:minmax(0,.4fr) minmax(0,.6fr);gap:48px;margin-top:20px;padding:96px 56px;align-items:start;border:1px solid var(--bd2);border-radius:28px;background:var(--panel)}.quickstart h2{margin:14px 0 20px;font-size:clamp(2rem,4vw,3.2rem);line-height:1.05;letter-spacing:-.03em}.quickstart p{color:var(--mut);line-height:1.65}.win{overflow:hidden;border:1px solid var(--bd2);border-radius:8px;background:#050d17;box-shadow:0 30px 60px -34px #000}.win header{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid #ffffff1a;color:#8aa3bc;font-size:12px}.win header span{display:flex;align-items:center;gap:8px}.win header i{width:11px;height:11px;border-radius:50%;display:inline-block}.r{background:#ff5f57}.y{background:#febc2e}.g{background:#28c840}.cp{border:1px solid #ffffff33;border-radius:3px;background:none;color:#8aa3bc;padding:5px 11px;font:500 11px ui-monospace,monospace;cursor:pointer}.cp.ok{background:var(--ac);border-color:var(--ac);color:var(--on)}pre{margin:0;padding:22px 24px;overflow-x:auto;color:#e8f1fa;font:13px/1.75 ui-monospace,monospace}.k{color:#ff7b72}.s{color:#a5d6ff}.f{color:#d2a8ff}.c{color:#6e7681;font-style:italic}.o{color:#ffa657}footer{padding:30px 36px;border-top:1px solid var(--bd);color:var(--mut);font-size:13px}footer div{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap}kbd{padding:1px 6px;border:1px solid var(--bd2);border-radius:3px;color:var(--fg)}[data-theme=light] .capability{background:#fff;border-color:#91acc7;box-shadow:0 16px 34px rgba(14,34,56,.11)}[data-theme=light] .capability strong{color:#0e2238}[data-theme=light] .capability small{color:#2f4660}
|
||
@media(max-width:1100px){.capabilities{grid-template-columns:repeat(3,minmax(140px,1fr))}.hero{padding-inline:32px}}@media(max-width:760px){.shell{display:block}.rail{position:relative;height:auto;min-height:0;padding:24px;border-right:0;border-bottom:1px solid var(--bd)}.logo{font-size:26px}.login{max-width:440px;margin:50px auto;transform:none}.rail-bottom{display:flex;justify-content:space-between;align-items:center}.tg{margin:0}.canvas{padding:14px}.hero{min-height:0;padding:70px 20px;border-radius:22px}.welcome{transform:none}.capabilities{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.capability{min-height:140px;border-radius:12px}.quickstart{grid-template-columns:1fr;margin-top:14px;padding:64px 24px;gap:30px;border-radius:22px}footer{padding:28px 10px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}.grid-glow{display:none}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="bar" id="bar"></div>
|
||
<div class="shell">
|
||
<aside class="rail"><a class="logo" href="#top"><img class="brand-mark" src="/TheCloud.png" alt=""><span class="brand-name">TheCloud</span></a><main class="login"><h1>Welcome back</h1><p class="lead">Continue with your Authentik account to open the TheCloud console.</p>${destination}${alert}${signIn}<p class="fine">Single sign-on. MFA happens there.</p></main><div class="rail-bottom"><div class="tg" role="group" aria-label="Theme"><button data-t="dark" aria-label="Dark theme">◐</button><button data-t="light" aria-label="Light theme">☼</button><button data-t="cyber" aria-label="Cyber theme">›_</button></div><span>TheCloud is powered by Floci.</span></div></aside>
|
||
<div class="canvas"><section class="hero" id="top"><div class="grid-glow" id="grid-glow"></div><div class="welcome"><h2 class="serif"><span class="prompt">$</span> <span id="tw"></span></h2><p class="sub">Your own cloud, on your own hardware.</p><div class="capabilities"><article class="capability"><img src="/aws-icons/Compute/Lambda.svg" alt=""><strong>Invoke functions</strong><small>Lambda</small></article><article class="capability"><img src="/aws-icons/Storage/Simple-Storage-Service.svg" alt=""><strong>Store objects</strong><small>S3</small></article><article class="capability"><img src="/aws-icons/App-Integration/Simple-Queue-Service.svg" alt=""><strong>Queue messages</strong><small>SQS</small></article><article class="capability"><img src="/aws-icons/App-Integration/Simple-Notification-Service.svg" alt=""><strong>Publish events</strong><small>SNS</small></article><article class="capability"><img src="/aws-icons/Database/DynamoDB.svg" alt=""><strong>Query tables</strong><small>DynamoDB</small></article><article class="capability"><img src="/aws-icons/App-Integration/Step-Functions.svg" alt=""><strong>Orchestrate workflows</strong><small>Step Functions</small></article></div><p class="note">Build and test against AWS services locally, with SSO in front.</p></div></section>
|
||
<section class="quickstart" id="quickstart"><div><span class="tag">quickstart</span><h2 class="serif">Same CLI. Different cloud.</h2><p>Point any AWS SDK or the AWS CLI at your TheCloud endpoint and your code runs unchanged, with nothing leaving your network.</p></div><div class="win"><header><span><i class="r"></i><i class="y"></i><i class="g"></i>quickstart.sh</span><button class="cp" id="cp" type="button">copy</button></header><pre><code id="code"><span class="c"># point the AWS CLI at TheCloud (sample values)</span>
|
||
<span class="k">export</span> AWS_ENDPOINT_URL=<span class="s">http://localhost:4566</span>
|
||
<span class="k">export</span> AWS_ACCESS_KEY_ID=<span class="s">test</span> AWS_SECRET_ACCESS_KEY=<span class="s">test</span>
|
||
<span class="k">export</span> AWS_DEFAULT_REGION=<span class="s">us-east-1</span>
|
||
|
||
<span class="f">aws</span> s3 mb s3://demo-bucket
|
||
<span class="f">aws</span> sqs create-queue <span class="o">--queue-name</span> fulfilment-queue
|
||
<span class="f">aws</span> stepfunctions list-state-machines</code></pre></div></section>
|
||
<footer class="mono"><div><span><span class="prompt">$</span> TheCloud</span><span>press <kbd>T</kbd> to switch theme</span></div></footer></div></div>
|
||
<script defer src="/login.js"></script>
|
||
</body>
|
||
</html>`
|
||
}
|
||
|
||
function escapeHtml(value: string): string {
|
||
return value.replace(/[&<>'"]/g, (character) => ({'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'}[character] ?? character))
|
||
}
|