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