feat: added a login page with Authentik authentication
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
type LoginPageOptions = {
|
||||
mode: 'local' | 'oidc'
|
||||
csrf: string
|
||||
returnTo: string
|
||||
error: string | null
|
||||
styleNonce: string
|
||||
}
|
||||
|
||||
export function renderLoginPage(options: LoginPageOptions): string {
|
||||
const destination = options.returnTo === '/'
|
||||
? ''
|
||||
: `<div class="dest mono on">Landing on <span>${escapeHtml(options.returnTo)}</span></div>`
|
||||
const alert = options.error ? `<div class="alert on" 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">
|
||||
<title>Sign in · TheCloud</title>
|
||||
<style nonce="${options.styleNonce}">
|
||||
:root[data-theme=dark]{--bg:#07111d;--el:#0c1a2b;--fg:#e8f1fa;--dim:#bdd0e3;--mut:#8aa3bc;--ac:#6cc4ff;--acr:108,196,255;--ac2:#ffb547;--bd:rgba(232,241,250,.09);--bd2:rgba(232,241,250,.2);--card:#0d1c2f;--code:#050d17;--on:#06223a;--err:#ff9a9a;--errbg:#3a1822}
|
||||
:root[data-theme=light]{--bg:#eaf2fa;--el:#f6faff;--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);--card:#fff;--code:#0e2238;--on:#fff;--err:#a4262c;--errbg:#fdeaea}
|
||||
:root[data-theme=cyber]{--bg:#02060a;--el:#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);--card:#050d10;--code:#000;--on:#02060a;--err:#ff8da1;--errbg:#2a0a14}
|
||||
*{box-sizing:border-box;margin:0}html{scroll-behavior:smooth}body{background:var(--bg);color:var(--fg);font-family:system-ui,"Segoe UI",sans-serif;overflow-x:hidden;-webkit-font-smoothing:antialiased}.mono{font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace}.serif{font-family:ui-serif,"Iowan Old Style","Palatino Linotype",Georgia,serif}[data-theme=cyber] body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:90;background:repeating-linear-gradient(0deg,transparent 0 2px,rgba(78,255,150,.03) 2px 3px)}body.fade,body.fade *{transition:background-color .5s,color .5s,border-color .5s!important}.bar{position:fixed;top:0;left:0;height:3px;width:0;z-index:100;background:linear-gradient(90deg,var(--ac),var(--ac2))}.wrap{max-width:1180px;margin:0 auto;padding:0 24px}
|
||||
nav{position:fixed;inset:0 0 auto;z-index:50;backdrop-filter:blur(14px);border-bottom:1px solid var(--bd);background:rgba(var(--acr),.03)}nav .wrap{display:flex;align-items:center;justify-content:space-between;height:64px}.logo{font-weight:700;font-size:18px;color:var(--fg);text-decoration:none;display:flex;align-items:center}.logo i{font-style:normal;color:var(--ac)}.blk{display:inline-block;width:.5em;height:1em;margin-left:4px;background:var(--ac);animation:bl 1.1s steps(1) infinite}@keyframes bl{50%{opacity:0}}.links{display:flex;gap:30px;font-size:14px}.links a{color:var(--dim);text-decoration:none;position:relative}.links a:hover{color:var(--fg)}.links a::after{content:"";position:absolute;left:0;bottom:-5px;height:1px;width:0;background:var(--ac);transition:width .3s}.links a:hover::after{width:100%}.tg{display:flex;gap:2px;padding:3px;border:1px solid var(--bd2);border-radius:99px;background:var(--el)}.tg button{width:32px;height:32px;border:0;border-radius:99px;background:none;color:var(--mut);cursor:pointer;display:grid;place-items:center;transition:.3s}.tg button:hover{color:var(--fg)}.tg button.on{background:var(--ac);color:var(--on);box-shadow:0 0 14px rgba(var(--acr),.5)}.tg button:focus-visible,.btn:focus-visible,.cp:focus-visible{outline:2px solid var(--fg);outline-offset:2px}
|
||||
.hero{position:relative;min-height:100vh;display:flex;align-items:center;padding:110px 0 70px;overflow:hidden;background-image:linear-gradient(var(--bd) 1px,transparent 1px),linear-gradient(90deg,var(--bd) 1px,transparent 1px);background-size:56px 56px;background-position:center}.dot{position:absolute;border-radius:50%;filter:blur(70px);opacity:.22;animation:dr 16s ease-in-out infinite;pointer-events:none}.d1{width:480px;height:480px;top:4%;left:-140px;background:var(--ac)}.d2{width:560px;height:560px;bottom:-200px;right:-180px;background:var(--ac2);animation-delay:-5s}@keyframes dr{33%{transform:translate(60px,-40px) scale(1.1)}66%{transform:translate(-30px,50px) scale(.95)}}.glow{position:absolute;width:680px;height:680px;border-radius:50%;background:radial-gradient(circle,rgba(var(--acr),.13),transparent 60%);transform:translate(-50%,-50%);pointer-events:none;opacity:0;transition:opacity .4s}.grid{position:relative;z-index:2;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,.75fr);gap:56px;align-items:center}.tag{display:inline-block;font:500 11px ui-monospace,Menlo,Consolas,monospace;letter-spacing:.1em;text-transform:uppercase;color:var(--ac);padding:4px 9px;border:1px solid rgba(var(--acr),.5);border-radius:2px;background:rgba(var(--acr),.08)}.meta{display:flex;gap:12px;flex-wrap:wrap;align-items:center;font-size:12px;color:var(--mut);margin-bottom:28px}.live{width:6px;height:6px;border-radius:50%;background:var(--ac2);box-shadow:0 0 8px var(--ac2);animation:pl 2s infinite}@keyframes pl{50%{opacity:.3}}h1{font-size:clamp(2rem,5.2vw,4.4rem);line-height:1.08;letter-spacing:-.04em;margin-bottom:26px;min-height:2.2em}h1 .p{color:var(--mut)}#tw::after{content:"▌";color:var(--ac);margin-left:4px;animation:bl 1s steps(1) infinite}.sub{font-size:clamp(1.2rem,2.2vw,1.7rem);line-height:1.35;font-style:italic;margin-bottom:34px;max-width:30em}.stats{display:flex;gap:40px;flex-wrap:wrap}.stats b{display:block;font-size:30px;color:var(--ac)}.stats span{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--mut)}
|
||||
.card{background:var(--card);border:1px solid var(--bd2);border-radius:8px;padding:32px;position:relative;box-shadow:0 30px 60px -34px rgba(0,0,0,.55)}.card::before{content:"";position:absolute;inset:0 0 auto;height:2px;background:linear-gradient(90deg,var(--ac),var(--ac2))}.card h2{font-size:26px;letter-spacing:-.02em;margin-bottom:8px}.lead{color:var(--mut);font-size:14.5px;line-height:1.55}.dest{display:none;margin-top:16px;padding:9px 12px;border:1px dashed var(--bd2);border-radius:6px;font-size:12.5px;color:var(--mut);overflow-wrap:anywhere}.dest.on{display:block}.dest span{color:var(--fg)}.alert{display:none;margin-top:18px;padding:12px 14px;border:1px solid currentColor;border-radius:6px;background:var(--errbg);color:var(--err);font-size:14px;line-height:1.45}.alert.on{display:block}.btn{margin-top:22px;width:100%;display:flex;align-items:center;justify-content:center;gap:10px;padding:15px 18px;border:0;border-radius:4px;background:var(--ac);color:var(--on);font:600 13px ui-monospace,Menlo,Consolas,monospace;letter-spacing:.08em;text-transform:uppercase;text-decoration:none;cursor:pointer;position:relative;overflow:hidden;transition:.3s}.btn::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.28),transparent);transform:translateX(-100%);transition:transform .6s}.btn:hover{transform:translateY(-2px);box-shadow:0 12px 30px -8px rgba(var(--acr),.6)}.btn:hover::before{transform:translateX(100%)}.btn:disabled{pointer-events:none;filter:saturate(.6)}.authentik-icon{position:relative;z-index:1;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)}}.nx{list-style:none;padding:22px 0 0;margin-top:26px;border-top:1px solid var(--bd);display:grid;gap:10px;font-size:14px;color:var(--mut)}.nx li::before{content:"→";color:var(--ac);margin-right:10px}.fine{margin-top:20px;font-size:12.5px;color:var(--mut);line-height:1.55}.local-form{display:grid;gap:8px;margin-top:18px}.local-form label{margin-top:6px;color:var(--mut);font:11px ui-monospace,Menlo,monospace;text-transform:uppercase;letter-spacing:.08em}.local-form input:not([type=hidden]){width:100%;min-height:43px;padding:0 12px;border:1px solid var(--bd2);border-radius:4px;outline:0;color:var(--fg);background:var(--bg)}.local-form input:focus{border-color:var(--ac);box-shadow:0 0 0 3px rgba(var(--acr),.14)}
|
||||
.mq{overflow:hidden;white-space:nowrap;padding:20px 0;border-block:1px solid var(--bd);background:var(--el)}.mq div{display:inline-flex;gap:56px;padding-right:56px;animation:sc 40s linear infinite;font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--mut)}@keyframes sc{to{transform:translateX(-50%)}}
|
||||
.qs{padding:100px 0;display:grid;grid-template-columns:minmax(0,.4fr) minmax(0,.6fr);gap:48px;align-items:start}.qs h2{font-size:clamp(2rem,4vw,3.2rem);line-height:1.05;letter-spacing:-.03em;margin:14px 0 20px}.qs p{color:var(--mut);line-height:1.65}.win{background:var(--code);border:1px solid var(--bd2);border-radius:8px;overflow:hidden;box-shadow:0 30px 60px -34px rgba(0,0,0,.5)}.win header{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid var(--bd);background:rgba(255,255,255,.04);font-size:12px;color:#8aa3bc}.win header span{display:flex;align-items:center;gap:8px}.win header .r{background:#ff5f57}.win header .y{background:#febc2e}.win header .g{background:#28c840}.pr{color:var(--ac)}.win header i{width:11px;height:11px;border-radius:50%;display:inline-block}.cp{background:none;border:1px solid var(--bd2);color:#8aa3bc;border-radius:3px;padding:5px 11px;font:500 11px ui-monospace,Menlo,monospace;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;transition:.25s}.cp:hover{color:var(--ac);border-color:var(--ac)}.cp.ok{background:var(--ac);color:var(--on);border-color:var(--ac)}pre{padding:22px 24px;overflow-x:auto;font-size:13px;line-height:1.75;color:#e8f1fa}.k{color:#ff7b72}.s{color:#a5d6ff}.f{color:#d2a8ff}.c{color:#6e7681;font-style:italic}.o{color:#ffa657}[data-theme=cyber] .k{color:#ff10f0}[data-theme=cyber] .s{color:#ffea00}[data-theme=cyber] .f{color:#8affb8}[data-theme=cyber] pre{color:#4eff96}
|
||||
footer{border-top:1px solid var(--bd);padding:36px 0;font-size:13px;color:var(--mut)}footer .wrap{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap}kbd{border:1px solid var(--bd2);border-radius:3px;padding:1px 6px;color:var(--fg)}::selection{background:var(--ac);color:var(--on)}
|
||||
@media(max-width:900px){.links{display:none}.grid,.qs{grid-template-columns:1fr}.grid{gap:36px}.hero{padding-top:96px}.stats{gap:26px}.qs{padding:64px 24px}}
|
||||
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bar" id="bar"></div>
|
||||
<nav><div class="wrap"><a class="logo mono" href="#top"><i>The</i>Cloud<span class="blk"></span></a><div class="links mono"><a href="#top">sign in</a><a href="#quickstart">quickstart</a></div><div class="tg" role="group" aria-label="Theme"><button data-t="dark" aria-label="Dark theme"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M21 13.5A9 9 0 1 1 10.5 3a7 7 0 0 0 10.5 10.5z"/></svg></button><button data-t="light" aria-label="Light theme"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button><button data-t="cyber" aria-label="Cyber theme"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l6 6-6 6M12 19h8"/></svg></button></div></div></nav>
|
||||
<section class="hero" id="top"><div class="dot d1"></div><div class="dot d2"></div><div class="glow" id="glow"></div><div class="wrap grid"><div><div class="meta mono"><span class="tag">powered by Floci</span><span>local AWS, your hardware</span></div><h1 class="mono"><span class="p">$</span> <span id="tw"></span></h1><p class="sub serif">Lambda, SQS, SNS, DynamoDB and Step Functions, running on your own network.</p><div class="stats mono"><div><b>7</b><span>workflow integrations</span></div><div><b>$0</b><span>aws bill</span></div><div><b>1</b><span>sign-in, sso + mfa</span></div></div></div><main class="card"><h2>Sign in</h2><p class="lead">Use your Authentik account to open the TheCloud console.</p>${destination}${alert}${signIn}<ul class="nx"><li>Browse and manage local AWS resources</li><li>Design state machines in Workflow Studio</li><li>Run executions and inspect every step</li></ul><p class="fine">TheCloud never sees your password. Sessions end after a period of inactivity.</p></main></div></section>
|
||||
<div class="mq mono" aria-hidden="true"><div id="mq"><span>Lambda</span><span>S3</span><span>SQS</span><span>SNS</span><span>DynamoDB</span><span>Step Functions</span><span>EventBridge</span><span>ECS</span><span>EC2</span><span>CloudWatch</span></div></div>
|
||||
<section class="wrap qs" 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 mono"><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 class="wrap"><span><span class="pr">$</span> TheCloud, powered by Floci</span><span>press <kbd>T</kbd> to switch theme</span></div></footer>
|
||||
<script nonce="${options.styleNonce}">(function(){var root=document.documentElement,btns=document.querySelectorAll('.tg button'),still=matchMedia('(prefers-reduced-motion: reduce)').matches;function setTheme(t,anim){if(anim){document.body.classList.add('fade');setTimeout(function(){document.body.classList.remove('fade')},600)}root.setAttribute('data-theme',t);try{localStorage.setItem('thecloud-theme',t)}catch(e){}btns.forEach(function(b){b.classList.toggle('on',b.dataset.t===t)})}var t0=matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';try{var sv=localStorage.getItem('thecloud-theme');if(sv==='dark'||sv==='light'||sv==='cyber')t0=sv}catch(e){}setTheme(t0,false);btns.forEach(function(b){b.addEventListener('click',function(){setTheme(b.dataset.t,true)})});document.addEventListener('keydown',function(e){if((e.key==='t'||e.key==='T')&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&!/INPUT|TEXTAREA/.test(e.target.tagName)){var o=['dark','light','cyber'];setTheme(o[(o.indexOf(root.getAttribute('data-theme'))+1)%3],true)}});var lines=['hello, engineer.','aws s3 mb s3://my-bucket','your own AWS, on your own hardware.','sfn start-execution --local','welcome to TheCloud.'],el=document.getElementById('tw'),g=0,c=0,del=false;if(still){el.textContent=lines[0]}else{(function tick(){var w=lines[g];if(del){c--;el.textContent=w.slice(0,c);if(c===0){del=false;g=(g+1)%lines.length;return setTimeout(tick,400)}return setTimeout(tick,35)}c++;el.textContent=w.slice(0,c);if(c===w.length){del=true;return setTimeout(tick,2200)}setTimeout(tick,75+Math.random()*50)})()}var bar=document.getElementById('bar');function prog(){var h=document.documentElement.scrollHeight-innerHeight;bar.style.width=(h>0?scrollY/h*100:0)+'%'}addEventListener('scroll',prog,{passive:true});prog();var hero=document.getElementById('top'),gl=document.getElementById('glow');hero.addEventListener('mousemove',function(e){var r=hero.getBoundingClientRect();gl.style.left=(e.clientX-r.left)+'px';gl.style.top=(e.clientY-r.top)+'px';gl.style.opacity='1'});hero.addEventListener('mouseleave',function(){gl.style.opacity='0'});var mq=document.getElementById('mq');mq.innerHTML+=mq.innerHTML;var cp=document.getElementById('cp');cp.addEventListener('click',function(){var txt=document.getElementById('code').innerText;function done(){cp.classList.add('ok');cp.textContent='copied';setTimeout(function(){cp.classList.remove('ok');cp.textContent='copy'},1800)}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).then(done,done)}else{done()}});var form=document.querySelector('[data-sso-form]');if(form){form.addEventListener('submit',function(){var go=form.querySelector('.btn'),label=document.getElementById('label');if(go.disabled)return;go.disabled=true;go.classList.add('is-loading');label.textContent='Redirecting to Authentik'})}addEventListener('pageshow',function(){var go=document.querySelector('[data-sso-form] .btn'),label=document.getElementById('label');if(go){go.disabled=false;go.classList.remove('is-loading')}if(label)label.textContent='Sign in with Authentik'})})();</script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value.replace(/[&<>'"]/g, (character) => ({'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'}[character] ?? character))
|
||||
}
|
||||
Reference in New Issue
Block a user