Login.tsx
Raw
1import { Layout } from "../layout.tsx";
2
3interface LoginProps {
4 error?: string;
5}
6
7export function Login({ error }: LoginProps) {
8 return (
9 <Layout user={null} title="Sign in">
10 <div class="auth-container">
11 <h1 class="page-title">Sign in</h1>
12 {error && <p class="form-error">{error}</p>}
13 <form method="POST" action="/login" class="auth-form">
14 <div class="form-group">
15 <label for="username">Username</label>
16 <input
17 id="username"
18 name="username"
19 type="text"
20 required
21 autocomplete="username"
22 />
23 </div>
24 <div id="passkey-section" style="display:none">
25 <button
26 id="passkey-btn"
27 class="btn btn-secondary btn-block"
28 type="button"
29 >
30 Sign in with passkey
31 </button>
32 <div class="auth-divider">
33 <span>or</span>
34 </div>
35 </div>
36 <div class="form-group">
37 <label for="password">Password</label>
38 <input
39 id="password"
40 name="password"
41 type="password"
42 required
43 autocomplete="current-password"
44 />
45 </div>
46 <button type="submit" class="btn btn-primary btn-block">
47 Sign in with password
48 </button>
49 </form>
50 <p class="auth-footer">
51 Don't have an account? <a href="/register">Register</a>
52 </p>
53 </div>
54 <script type="module">{`
55 import { startAuthentication } from '/assets/simplewebauthn-browser.js';
56 document.getElementById('passkey-section').style.display = 'block';
57 document.getElementById('passkey-btn').addEventListener('click', async () => {
58 try {
59 const optsResp = await fetch('/auth/passkey/login/options', { method: 'POST' });
60 const opts = await optsResp.json();
61 const result = await startAuthentication({ optionsJSON: opts });
62 const verResp = await fetch('/auth/passkey/login/verify', {
63 method: 'POST',
64 headers: { 'Content-Type': 'application/json' },
65 body: JSON.stringify(result),
66 });
67 if (verResp.ok) {
68 window.location.href = '/';
69 } else {
70 const err = await verResp.json();
71 alert(err.error ?? 'Passkey sign in failed');
72 }
73 } catch (e) {
74 alert('Passkey sign in failed: ' + e.message);
75 }
76 });
77 `}</script>
78 </Layout>
79 );
80}
81