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