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