Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

signin-v2.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

signin-v2.tsxBlame623 lines · 1 contributor
11c3ab6ccanty labs1import type { FC } from "hono/jsx";
2
3export interface SignInV2Props {
4 redirect?: string;
5 error?: string;
cf21786ccanty labs6 csrfToken?: string;
11c3ab6ccanty labs7}
8
9export const SignInV2: FC<SignInV2Props> = (props) => {
cf21786ccanty labs10 const { redirect = "", error = "", csrfToken } = props;
11c3ab6ccanty labs11
12 return (
13 <>
14 <style dangerouslySetInnerHTML={{ __html: css }} />
15 <div class="si-root">
16 {/* Left: form panel */}
17 <div class="si-left">
18 <a href="/" class="si-logo">
19 <span class="si-logo-mark"></span>gluecron
20 </a>
21
22 <div class="si-form-wrap">
23 <h1 class="si-heading">Sign in</h1>
24 <p class="si-subheading">Use the identity you already have — no new passwords.</p>
25
26 {error && (
27 <div class="si-error-banner">{error}</div>
28 )}
29
30 {redirect && (
31 <input type="hidden" name="redirect" value={redirect} />
32 )}
33
34 <div class="si-oauth-group">
35 <a href="/auth/github" class="si-btn si-btn-github">
36 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
37 <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
38 </svg>
39 Continue with GitHub
40 </a>
41
42 <a href="/auth/google" class="si-btn si-btn-secondary">
43 <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
44 <path fill="#4285F4" d="M23.5 12.27c0-.85-.08-1.66-.22-2.45H12v4.63h6.45a5.52 5.52 0 0 1-2.39 3.62v3h3.87c2.26-2.09 3.57-5.16 3.57-8.8z" />
45 <path fill="#34A853" d="M12 24c3.24 0 5.96-1.07 7.94-2.91l-3.87-3c-1.07.72-2.45 1.15-4.07 1.15-3.13 0-5.78-2.11-6.73-4.96H1.29v3.1A12 12 0 0 0 12 24z" />
46 <path fill="#FBBC05" d="M5.27 14.28a7.2 7.2 0 0 1 0-4.56v-3.1H1.29a12 12 0 0 0 0 10.76l3.98-3.1z" />
47 <path fill="#EA4335" d="M12 4.76c1.76 0 3.34.61 4.59 1.8l3.44-3.44C17.95 1.19 15.24 0 12 0A12 12 0 0 0 1.29 6.62l3.98 3.1C6.22 6.87 8.87 4.76 12 4.76z" />
48 </svg>
49 Continue with Google
50 </a>
51
cf21786ccanty labs52 <button
53 id="pk-signin-btn"
54 type="button"
55 class="si-btn si-btn-secondary"
56 data-redirect={redirect || "/"}
57 >
11c3ab6ccanty labs58 <span class="si-passkey-icon" aria-hidden="true">⌘</span>
59 Continue with a passkey
cf21786ccanty labs60 </button>
61 <p id="pk-signin-status" class="si-passkey-status" aria-live="polite"></p>
11c3ab6ccanty labs62 </div>
63
64 <div class="si-divider">
65 <span class="si-divider-line"></span>
66 <span class="si-divider-label">OR</span>
67 <span class="si-divider-line"></span>
68 </div>
69
70 <div class="si-magic-row">
71 <input
72 id="si-email-input"
73 type="email"
74 placeholder="work@company.com"
75 class="si-email-input"
76 autocomplete="email"
77 />
78 <button
79 id="si-magic-btn"
80 type="button"
81 class="si-magic-btn"
82 >
83 Email me a link
84 </button>
85 </div>
86 <p class="si-magic-hint">Magic link — no password, expires in 10 minutes.</p>
87
88 <div class="si-aux-links">
89 <a href="/sso" class="si-aux-link si-aux-link-accent">Enterprise SSO (Okta, Azure AD, Google Workspace) →</a>
90 <a href="/keys" class="si-aux-link">Just cloning? SSH keys work without signing in →</a>
91 <a href="/play" class="si-aux-link">Try without an account at /play →</a>
92 </div>
93 </div>
94
95 <p class="si-footer-note">TLS terminated on our own metal · no third-party proxy in the auth loop</p>
96 </div>
97
98 {/* Right: proof panel */}
99 <div class="si-right">
100 <div class="si-grid-overlay"></div>
101 <div class="si-proof-inner">
102 <div class="si-proof-eyebrow">Why teams switch</div>
103 <h2 class="si-proof-heading">Sign in once.<br />Ship without waiting, forever.</h2>
104 <div class="si-props-list">
105 <div class="si-prop-item">
106 <span class="si-green-dot"></span>
107 <p class="si-prop-text">Sonnet 5 reviews every PR the moment it opens — with a manual path that's always one click away.</p>
108 </div>
109 <div class="si-prop-item">
110 <span class="si-green-dot"></span>
111 <p class="si-prop-text">Failures repair themselves in isolated sandboxes; everything is audited and reversible in one click.</p>
112 </div>
113 <div class="si-prop-item">
114 <span class="si-green-dot"></span>
115 <p class="si-prop-text">Continuous two-way GitHub mirror — trial for months, cut over when you're ready, leave any time.</p>
116 </div>
117 </div>
118 <div class="si-proof-footer">quality numbers published live · gluecron.com/trust</div>
119 </div>
120 </div>
121 </div>
122
123 <script dangerouslySetInnerHTML={{ __html: js }} />
124 </>
125 );
126};
127
128export default SignInV2;
129
130const js = `
131(function () {
cf21786ccanty labs132 /* ── Magic-link button ── */
11c3ab6ccanty labs133 var emailInput = document.getElementById('si-email-input');
134 var magicBtn = document.getElementById('si-magic-btn');
cf21786ccanty labs135 if (emailInput && magicBtn) {
136 var sent = false;
137 function updateBtn() {
138 if (sent) {
139 magicBtn.textContent = '\\u2713 Link sent';
140 magicBtn.classList.add('si-magic-btn--sent');
141 } else {
142 magicBtn.textContent = 'Email me a link';
143 magicBtn.classList.remove('si-magic-btn--sent');
144 }
11c3ab6ccanty labs145 }
cf21786ccanty labs146 emailInput.addEventListener('input', function () {
147 if (sent) { sent = false; updateBtn(); }
148 });
149 magicBtn.addEventListener('click', function () {
150 var val = emailInput.value;
151 if (val && val.indexOf('@') !== -1) {
152 sent = true;
153 updateBtn();
154 } else {
155 emailInput.focus();
156 emailInput.classList.add('si-email-input--shake');
157 setTimeout(function () { emailInput.classList.remove('si-email-input--shake'); }, 400);
158 }
159 });
11c3ab6ccanty labs160 }
161
cf21786ccanty labs162 /* ── Passkey / WebAuthn ── */
163 var pkBtn = document.getElementById('pk-signin-btn');
164 var pkStatus = document.getElementById('pk-signin-status');
165 if (!pkBtn || !pkStatus) return;
166
167 function b64uToBuf(s) {
168 s = s.replace(/-/g,'+').replace(/_/g,'/');
169 while (s.length % 4) s += '=';
170 var bin = atob(s);
171 var buf = new Uint8Array(bin.length);
172 for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
173 return buf.buffer;
174 }
175 function bufToB64u(buf) {
176 var bytes = new Uint8Array(buf), bin = '';
177 for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
178 return btoa(bin).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'');
179 }
11c3ab6ccanty labs180
cf21786ccanty labs181 pkBtn.addEventListener('click', async function () {
182 if (!window.PublicKeyCredential) {
183 pkStatus.textContent = 'Passkeys are not supported in this browser.';
184 return;
185 }
186 var redirect = pkBtn.dataset.redirect || '/';
187 pkStatus.textContent = 'Preparing\\u2026';
188 try {
189 var optsRes = await fetch('/api/passkeys/auth/options', {
190 method: 'POST',
191 headers: { 'content-type': 'application/json' },
192 body: JSON.stringify({})
193 });
194 if (!optsRes.ok) throw new Error('options request failed');
195 var data = await optsRes.json();
196 var options = data.options, sessionKey = data.sessionKey;
197 options.challenge = b64uToBuf(options.challenge);
198 if (options.allowCredentials) {
199 options.allowCredentials = options.allowCredentials.map(function (c) {
200 return Object.assign({}, c, { id: b64uToBuf(c.id) });
201 });
202 }
203 pkStatus.textContent = 'Touch your authenticator\\u2026';
204 var cred = await navigator.credentials.get({ publicKey: options });
205 var resp = {
206 id: cred.id,
207 rawId: bufToB64u(cred.rawId),
208 type: cred.type,
209 response: {
210 clientDataJSON: bufToB64u(cred.response.clientDataJSON),
211 authenticatorData: bufToB64u(cred.response.authenticatorData),
212 signature: bufToB64u(cred.response.signature),
213 userHandle: cred.response.userHandle ? bufToB64u(cred.response.userHandle) : null
214 },
215 clientExtensionResults: cred.getClientExtensionResults ? cred.getClientExtensionResults() : {}
216 };
217 var verifyRes = await fetch('/api/passkeys/auth/verify', {
218 method: 'POST',
219 headers: { 'content-type': 'application/json' },
220 body: JSON.stringify({ sessionKey: sessionKey, response: resp })
221 });
222 if (!verifyRes.ok) {
223 var j = await verifyRes.json().catch(function () { return {}; });
224 throw new Error(j.error || 'verify failed');
225 }
226 pkStatus.textContent = 'Signed in. Redirecting\\u2026';
227 window.location.href = redirect;
228 } catch (e) {
229 pkStatus.textContent = 'Error: ' + (e && e.message ? e.message : String(e));
11c3ab6ccanty labs230 }
231 });
232})();
233`;
234
235const css = `
236@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
237
238*, *::before, *::after { box-sizing: border-box; }
239
240.si-root {
241 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
242 font-size: 14px;
243 line-height: 1.55;
244 letter-spacing: -0.008em;
245 color: #16181d;
246 background: #fcfcfd;
247 min-height: 100vh;
248 display: grid;
249 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
250 -webkit-font-smoothing: antialiased;
251 text-rendering: optimizeLegibility;
252}
253
254/* ── Left panel ── */
255
256.si-left {
257 display: flex;
258 flex-direction: column;
259 padding: 28px 32px;
260 min-height: 100vh;
261}
262
263.si-logo {
264 display: inline-flex;
265 align-items: center;
266 gap: 9px;
267 font-family: 'Inter Tight', sans-serif;
268 font-weight: 600;
269 font-size: 15px;
270 letter-spacing: -0.02em;
271 color: #16181d;
272 text-decoration: none;
273 align-self: flex-start;
274}
275
276.si-logo-mark {
277 width: 16px;
278 height: 16px;
279 border-radius: 5px;
280 background: #4353c9;
281 display: inline-block;
282 flex-shrink: 0;
283}
284
285.si-form-wrap {
286 flex: 1;
287 display: flex;
288 flex-direction: column;
289 justify-content: center;
290 max-width: 380px;
291 width: 100%;
292 margin: 0 auto;
293 padding: 48px 0;
294}
295
296.si-heading {
297 font-family: 'Inter Tight', sans-serif;
298 font-size: 26px;
299 font-weight: 600;
300 letter-spacing: -0.024em;
301 line-height: 1.15;
302 margin: 0 0 6px;
303 color: #111318;
304}
305
306.si-subheading {
307 font-size: 13.5px;
308 color: #6b7080;
309 margin: 0 0 28px;
310}
311
312.si-error-banner {
313 background: rgba(180, 35, 24, 0.08);
314 border: 1px solid rgba(180, 35, 24, 0.18);
315 border-radius: 10px;
316 color: #b42318;
317 font-size: 13.5px;
318 padding: 10px 14px;
319 margin-bottom: 18px;
320}
321
322/* ── OAuth buttons ── */
323
324.si-oauth-group {
325 display: flex;
326 flex-direction: column;
327 gap: 10px;
328}
329
330.si-btn {
331 display: flex;
332 align-items: center;
333 justify-content: center;
334 gap: 10px;
335 padding: 11px 16px;
336 border-radius: 10px;
337 font-size: 14px;
338 font-weight: 600;
339 font-family: 'Inter', sans-serif;
340 text-decoration: none;
341 cursor: pointer;
342 transition: box-shadow 0.15s ease, border-color 0.15s ease;
343 letter-spacing: -0.008em;
344}
345
346.si-btn-github {
347 background: #16181d;
348 color: #fff;
349 border: 1px solid #16181d;
350}
351
352.si-btn-github:hover {
353 box-shadow: 0 8px 22px rgba(22, 24, 29, 0.18);
354}
355
356.si-btn-secondary {
357 background: #ffffff;
358 color: #16181d;
359 border: 1px solid rgba(22, 24, 29, 0.14);
360}
361
362.si-btn-secondary:hover {
363 border-color: rgba(22, 24, 29, 0.30);
364}
365
366.si-passkey-icon {
367 font-size: 15px;
368 line-height: 1;
369}
370
cf21786ccanty labs371.si-passkey-status {
372 font-size: 12px;
373 color: #6b7080;
374 margin: 4px 0 0;
375 min-height: 16px;
376 text-align: center;
377}
378
11c3ab6ccanty labs379/* ── OR divider ── */
380
381.si-divider {
382 display: flex;
383 align-items: center;
384 gap: 14px;
385 margin: 22px 0;
386}
387
388.si-divider-line {
389 flex: 1;
390 height: 1px;
391 background: rgba(22, 24, 29, 0.08);
392}
393
394.si-divider-label {
395 font-size: 11.5px;
396 color: #8a8d99;
397 font-family: 'JetBrains Mono', monospace;
398 letter-spacing: 0.08em;
399}
400
401/* ── Magic link row ── */
402
403.si-magic-row {
404 display: flex;
405 gap: 8px;
406}
407
408.si-email-input {
409 flex: 1;
410 border: 1px solid rgba(22, 24, 29, 0.12);
411 border-radius: 10px;
412 padding: 10px 14px;
413 outline: none;
414 font-family: 'Inter', sans-serif;
415 font-size: 14px;
416 letter-spacing: -0.008em;
417 color: #16181d;
418 background: #ffffff;
419 min-width: 0;
420 transition: border-color 0.15s ease, box-shadow 0.15s ease;
421}
422
423.si-email-input:focus {
424 border-color: rgba(22, 24, 29, 0.28);
425 box-shadow: 0 0 0 3px rgba(67, 83, 201, 0.10);
426}
427
428.si-email-input::placeholder {
429 color: #c4c6cf;
430}
431
432@keyframes si-shake {
433 0%, 100% { transform: translateX(0); }
434 20% { transform: translateX(-4px); }
435 40% { transform: translateX(4px); }
436 60% { transform: translateX(-3px); }
437 80% { transform: translateX(3px); }
438}
439
440.si-email-input--shake {
441 animation: si-shake 0.4s ease;
442 border-color: rgba(180, 35, 24, 0.40);
443}
444
445.si-magic-btn {
446 padding: 10px 16px;
447 border-radius: 10px;
448 font-size: 13.5px;
449 font-weight: 600;
450 font-family: 'Inter', sans-serif;
451 letter-spacing: -0.008em;
452 border: 1px solid rgba(22, 24, 29, 0.14);
453 background: #ffffff;
454 color: #16181d;
455 cursor: pointer;
456 white-space: nowrap;
457 transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
458}
459
460.si-magic-btn:hover {
461 border-color: rgba(22, 24, 29, 0.28);
462}
463
464.si-magic-btn--sent {
465 background: #1e7f5c;
466 color: #ffffff;
467 border-color: #1e7f5c;
468}
469
470.si-magic-btn--sent:hover {
471 border-color: #1e7f5c;
472}
473
474.si-magic-hint {
475 font-size: 12px;
476 color: #8a8d99;
477 margin: 10px 0 0;
478}
479
480/* ── Auxiliary links ── */
481
482.si-aux-links {
483 border-top: 1px solid rgba(22, 24, 29, 0.07);
484 margin-top: 28px;
485 padding-top: 20px;
486 display: flex;
487 flex-direction: column;
488 gap: 8px;
489}
490
491.si-aux-link {
492 font-size: 13px;
493 color: #6b7080;
494 text-decoration: none;
495 transition: color 0.12s ease;
496}
497
498.si-aux-link:hover {
499 color: #16181d;
500}
501
502.si-aux-link-accent {
503 color: #4353c9;
504}
505
506.si-aux-link-accent:hover {
507 color: #3544b0;
508 text-decoration: underline;
509}
510
511/* ── Footer note ── */
512
513.si-footer-note {
514 font-size: 11.5px;
515 color: #8a8d99;
516 margin: 0;
517 font-family: 'JetBrains Mono', monospace;
518 letter-spacing: 0.01em;
519}
520
521/* ── Right panel ── */
522
523.si-right {
524 background: radial-gradient(130% 130% at 50% -10%, #1b2030 0%, #12151f 45%, #0b0d13 100%);
525 color: #fff;
526 display: flex;
527 flex-direction: column;
528 justify-content: center;
529 padding: 64px 56px;
530 position: relative;
531 overflow: hidden;
532}
533
534.si-grid-overlay {
535 position: absolute;
536 inset: 0;
537 opacity: 0.35;
538 background-image:
539 linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
540 linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
541 background-size: 56px 56px;
542 -webkit-mask-image: radial-gradient(90% 90% at 50% 0%, #000, transparent 80%);
543 mask-image: radial-gradient(90% 90% at 50% 0%, #000, transparent 80%);
544 pointer-events: none;
545}
546
547.si-proof-inner {
548 position: relative;
549 max-width: 440px;
550}
551
552.si-proof-eyebrow {
553 font-family: 'JetBrains Mono', monospace;
554 font-size: 11px;
555 letter-spacing: 0.12em;
556 text-transform: uppercase;
557 color: #a9b4ee;
558 margin-bottom: 16px;
559}
560
561.si-proof-heading {
562 font-family: 'Inter Tight', sans-serif;
563 font-size: 30px;
564 font-weight: 600;
565 letter-spacing: -0.024em;
566 line-height: 1.15;
567 margin: 0 0 28px;
568 color: #fff;
569}
570
571.si-props-list {
572 display: flex;
573 flex-direction: column;
574 gap: 18px;
575}
576
577.si-prop-item {
578 display: flex;
579 gap: 12px;
580 align-items: flex-start;
581}
582
583.si-green-dot {
584 width: 7px;
585 height: 7px;
586 border-radius: 50%;
587 background: #1e7f5c;
588 flex-shrink: 0;
589 margin-top: 7px;
590}
591
592.si-prop-text {
593 font-size: 14px;
594 color: rgba(255, 255, 255, 0.78);
595 margin: 0;
596 line-height: 1.6;
597}
598
599.si-proof-footer {
600 border-top: 1px solid rgba(255, 255, 255, 0.10);
601 margin-top: 32px;
602 padding-top: 20px;
603 font-family: 'JetBrains Mono', monospace;
604 font-size: 11.5px;
605 color: rgba(255, 255, 255, 0.45);
606}
607
608/* ── Responsive ── */
609
610@media (max-width: 768px) {
611 .si-root {
612 grid-template-columns: 1fr;
613 }
614
615 .si-right {
616 display: none;
617 }
618
619 .si-left {
620 min-height: 100vh;
621 }
622}
623`;