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.tsxBlame699 lines · 2 contributors
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
8102dd4ccantynz-alt273/* This view ships its own standalone stylesheet (it does not go through
274 layout.tsx), so the browser default body margin of 8px still applied and
275 pushed the document 8px wider than the viewport on mobile. */
276html, body { margin: 0; padding: 0; }
277
11c3ab6ccanty labs278.si-root {
279 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
280 font-size: 14px;
281 line-height: 1.55;
282 letter-spacing: -0.008em;
283 color: #16181d;
284 background: #fcfcfd;
285 min-height: 100vh;
286 display: grid;
287 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
288 -webkit-font-smoothing: antialiased;
289 text-rendering: optimizeLegibility;
290}
291
292/* ── Left panel ── */
293
294.si-left {
295 display: flex;
296 flex-direction: column;
297 padding: 28px 32px;
298 min-height: 100vh;
8102dd4ccantynz-alt299 /* Grid/flex items default to min-width:auto, so .si-magic-row
300 ("Email me a link", min-content 358px) floored this column at 422px and
301 forced a horizontal scrollbar at 390px. Let it shrink instead. */
302 min-width: 0;
11c3ab6ccanty labs303}
304
305.si-logo {
306 display: inline-flex;
307 align-items: center;
308 gap: 9px;
309 font-family: 'Inter Tight', sans-serif;
310 font-weight: 600;
311 font-size: 15px;
312 letter-spacing: -0.02em;
313 color: #16181d;
314 text-decoration: none;
315 align-self: flex-start;
316}
317
318.si-logo-mark {
319 width: 16px;
320 height: 16px;
321 border-radius: 5px;
322 background: #4353c9;
323 display: inline-block;
324 flex-shrink: 0;
325}
326
327.si-form-wrap {
328 flex: 1;
329 display: flex;
330 flex-direction: column;
331 justify-content: center;
332 max-width: 380px;
333 width: 100%;
8102dd4ccantynz-alt334 min-width: 0;
11c3ab6ccanty labs335 margin: 0 auto;
336 padding: 48px 0;
337}
338
339.si-heading {
340 font-family: 'Inter Tight', sans-serif;
341 font-size: 26px;
342 font-weight: 600;
343 letter-spacing: -0.024em;
344 line-height: 1.15;
345 margin: 0 0 6px;
346 color: #111318;
347}
348
349.si-subheading {
350 font-size: 13.5px;
351 color: #6b7080;
352 margin: 0 0 28px;
353}
354
355.si-error-banner {
356 background: rgba(180, 35, 24, 0.08);
357 border: 1px solid rgba(180, 35, 24, 0.18);
358 border-radius: 10px;
359 color: #b42318;
360 font-size: 13.5px;
361 padding: 10px 14px;
362 margin-bottom: 18px;
363}
364
2aefa37ccanty labs365/* ── Password form ── */
366
367.si-login-form {
368 display: flex;
369 flex-direction: column;
370 margin: 0 0 4px;
371}
372
373.si-field-label {
374 font-size: 12.5px;
375 font-weight: 600;
376 color: #3a3d47;
377 margin: 10px 0 5px;
378}
379
380.si-submit {
381 width: 100%;
382 margin-top: 16px;
383 border: 0;
384}
385
386.si-forgot {
387 font-size: 12.5px;
388 color: #6b7080;
389 text-decoration: none;
390 margin-top: 12px;
391 align-self: flex-start;
392}
393
394.si-forgot:hover {
395 color: #4353c9;
396}
397
11c3ab6ccanty labs398/* ── OAuth buttons ── */
399
400.si-oauth-group {
401 display: flex;
402 flex-direction: column;
403 gap: 10px;
404}
405
406.si-btn {
407 display: flex;
408 align-items: center;
409 justify-content: center;
410 gap: 10px;
411 padding: 11px 16px;
412 border-radius: 10px;
413 font-size: 14px;
414 font-weight: 600;
415 font-family: 'Inter', sans-serif;
416 text-decoration: none;
417 cursor: pointer;
418 transition: box-shadow 0.15s ease, border-color 0.15s ease;
419 letter-spacing: -0.008em;
420}
421
422.si-btn-github {
423 background: #16181d;
424 color: #fff;
425 border: 1px solid #16181d;
426}
427
428.si-btn-github:hover {
429 box-shadow: 0 8px 22px rgba(22, 24, 29, 0.18);
430}
431
432.si-btn-secondary {
433 background: #ffffff;
434 color: #16181d;
435 border: 1px solid rgba(22, 24, 29, 0.14);
436}
437
438.si-btn-secondary:hover {
439 border-color: rgba(22, 24, 29, 0.30);
440}
441
442.si-passkey-icon {
443 font-size: 15px;
444 line-height: 1;
445}
446
cf21786ccanty labs447.si-passkey-status {
448 font-size: 12px;
449 color: #6b7080;
450 margin: 4px 0 0;
451 min-height: 16px;
452 text-align: center;
453}
454
11c3ab6ccanty labs455/* ── OR divider ── */
456
457.si-divider {
458 display: flex;
459 align-items: center;
460 gap: 14px;
461 margin: 22px 0;
462}
463
464.si-divider-line {
465 flex: 1;
466 height: 1px;
467 background: rgba(22, 24, 29, 0.08);
468}
469
470.si-divider-label {
471 font-size: 11.5px;
472 color: #8a8d99;
473 font-family: 'JetBrains Mono', monospace;
474 letter-spacing: 0.08em;
475}
476
477/* ── Magic link row ── */
478
479.si-magic-row {
480 display: flex;
481 gap: 8px;
482}
483
484.si-email-input {
485 flex: 1;
486 border: 1px solid rgba(22, 24, 29, 0.12);
487 border-radius: 10px;
488 padding: 10px 14px;
489 outline: none;
490 font-family: 'Inter', sans-serif;
491 font-size: 14px;
492 letter-spacing: -0.008em;
493 color: #16181d;
494 background: #ffffff;
495 min-width: 0;
496 transition: border-color 0.15s ease, box-shadow 0.15s ease;
497}
498
499.si-email-input:focus {
500 border-color: rgba(22, 24, 29, 0.28);
501 box-shadow: 0 0 0 3px rgba(67, 83, 201, 0.10);
502}
503
504.si-email-input::placeholder {
505 color: #c4c6cf;
506}
507
508@keyframes si-shake {
509 0%, 100% { transform: translateX(0); }
510 20% { transform: translateX(-4px); }
511 40% { transform: translateX(4px); }
512 60% { transform: translateX(-3px); }
513 80% { transform: translateX(3px); }
514}
515
516.si-email-input--shake {
517 animation: si-shake 0.4s ease;
518 border-color: rgba(180, 35, 24, 0.40);
519}
520
521.si-magic-btn {
522 padding: 10px 16px;
523 border-radius: 10px;
524 font-size: 13.5px;
525 font-weight: 600;
526 font-family: 'Inter', sans-serif;
527 letter-spacing: -0.008em;
528 border: 1px solid rgba(22, 24, 29, 0.14);
529 background: #ffffff;
530 color: #16181d;
531 cursor: pointer;
532 white-space: nowrap;
533 transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
534}
535
536.si-magic-btn:hover {
537 border-color: rgba(22, 24, 29, 0.28);
538}
539
540.si-magic-btn--sent {
541 background: #1e7f5c;
542 color: #ffffff;
543 border-color: #1e7f5c;
544}
545
546.si-magic-btn--sent:hover {
547 border-color: #1e7f5c;
548}
549
550.si-magic-hint {
551 font-size: 12px;
552 color: #8a8d99;
553 margin: 10px 0 0;
554}
555
556/* ── Auxiliary links ── */
557
558.si-aux-links {
559 border-top: 1px solid rgba(22, 24, 29, 0.07);
560 margin-top: 28px;
561 padding-top: 20px;
562 display: flex;
563 flex-direction: column;
564 gap: 8px;
565}
566
567.si-aux-link {
568 font-size: 13px;
569 color: #6b7080;
570 text-decoration: none;
571 transition: color 0.12s ease;
572}
573
574.si-aux-link:hover {
575 color: #16181d;
576}
577
578.si-aux-link-accent {
579 color: #4353c9;
580}
581
582.si-aux-link-accent:hover {
583 color: #3544b0;
584 text-decoration: underline;
585}
586
587/* ── Footer note ── */
588
589.si-footer-note {
590 font-size: 11.5px;
591 color: #8a8d99;
592 margin: 0;
593 font-family: 'JetBrains Mono', monospace;
594 letter-spacing: 0.01em;
595}
596
597/* ── Right panel ── */
598
599.si-right {
600 background: radial-gradient(130% 130% at 50% -10%, #1b2030 0%, #12151f 45%, #0b0d13 100%);
601 color: #fff;
602 display: flex;
603 flex-direction: column;
604 justify-content: center;
605 padding: 64px 56px;
606 position: relative;
607 overflow: hidden;
608}
609
610.si-grid-overlay {
611 position: absolute;
612 inset: 0;
613 opacity: 0.35;
614 background-image:
615 linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
616 linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
617 background-size: 56px 56px;
618 -webkit-mask-image: radial-gradient(90% 90% at 50% 0%, #000, transparent 80%);
619 mask-image: radial-gradient(90% 90% at 50% 0%, #000, transparent 80%);
620 pointer-events: none;
621}
622
623.si-proof-inner {
624 position: relative;
625 max-width: 440px;
626}
627
628.si-proof-eyebrow {
629 font-family: 'JetBrains Mono', monospace;
630 font-size: 11px;
631 letter-spacing: 0.12em;
632 text-transform: uppercase;
633 color: #a9b4ee;
634 margin-bottom: 16px;
635}
636
637.si-proof-heading {
638 font-family: 'Inter Tight', sans-serif;
639 font-size: 30px;
640 font-weight: 600;
641 letter-spacing: -0.024em;
642 line-height: 1.15;
643 margin: 0 0 28px;
644 color: #fff;
645}
646
647.si-props-list {
648 display: flex;
649 flex-direction: column;
650 gap: 18px;
651}
652
653.si-prop-item {
654 display: flex;
655 gap: 12px;
656 align-items: flex-start;
657}
658
659.si-green-dot {
660 width: 7px;
661 height: 7px;
662 border-radius: 50%;
663 background: #1e7f5c;
664 flex-shrink: 0;
665 margin-top: 7px;
666}
667
668.si-prop-text {
669 font-size: 14px;
670 color: rgba(255, 255, 255, 0.78);
671 margin: 0;
672 line-height: 1.6;
673}
674
675.si-proof-footer {
676 border-top: 1px solid rgba(255, 255, 255, 0.10);
677 margin-top: 32px;
678 padding-top: 20px;
679 font-family: 'JetBrains Mono', monospace;
680 font-size: 11.5px;
681 color: rgba(255, 255, 255, 0.45);
682}
683
684/* ── Responsive ── */
685
686@media (max-width: 768px) {
687 .si-root {
688 grid-template-columns: 1fr;
689 }
690
691 .si-right {
692 display: none;
693 }
694
695 .si-left {
696 min-height: 100vh;
697 }
698}
699`;