Register.tsx
Raw
1import { Layout } from "../layout.tsx";
2
3interface RegisterProps {
4 error?: string;
5 question?: string;
6 pending?: boolean;
7}
8
9export function Register({ error, question, pending }: RegisterProps) {
10 if (pending) {
11 return (
12 <Layout user={null} title="Register">
13 <div class="auth-container">
14 <h1 class="page-title">Create account</h1>
15 <p class="form-success">
16 Your account has been submitted for review. You will be
17 able to log in once an admin approves it.
18 </p>
19 <p class="auth-footer">
20 Already have an account? <a href="/login">Sign in</a>
21 </p>
22 </div>
23 </Layout>
24 );
25 }
26
27 return (
28 <Layout user={null} title="Register">
29 <div class="auth-container">
30 <h1 class="page-title">Create account</h1>
31 {error && <p class="form-error">{error}</p>}
32 <form method="POST" action="/register" class="auth-form">
33 <div class="form-group">
34 <label for="username">Username</label>
35 <input
36 id="username"
37 name="username"
38 type="text"
39 required
40 autocomplete="username"
41 pattern="[a-zA-Z0-9_-]+"
42 title="Letters, numbers, hyphens and underscores only"
43 />
44 </div>
45 {question && (
46 <div class="form-group">
47 <label for="application">{question}</label>
48 <textarea
49 id="application"
50 name="application"
51 rows="4"
52 required
53 placeholder="Your answer..."
54 ></textarea>
55 </div>
56 )}
57 <div id="passkey-section" style="display:none">
58 <button
59 id="passkey-register-btn"
60 class="btn btn-secondary btn-block"
61 type="button"
62 >
63 Register with passkey
64 </button>
65 <div class="auth-divider">
66 <span>or</span>
67 </div>
68 </div>
69 <div class="form-group">
70 <label for="password">Password</label>
71 <input
72 id="password"
73 name="password"
74 type="password"
75 autocomplete="new-password"
76 minlength="8"
77 />
78 </div>
79 <div class="form-group">
80 <label for="password2">Confirm password</label>
81 <input
82 id="password2"
83 name="password2"
84 type="password"
85 autocomplete="new-password"
86 minlength="8"
87 />
88 </div>
89 <button type="submit" class="btn btn-primary btn-block">
90 Register with password
91 </button>
92 </form>
93 <p class="auth-footer">
94 Already have an account? <a href="/login">Sign in</a>
95 </p>
96 </div>
97 <script type="module">{`
98 import { startRegistration } from '/assets/simplewebauthn-browser.js';
99 const usernameInput = document.getElementById('username');
100 const applicationInput = document.getElementById('application');
101 document.getElementById('passkey-section').style.display = 'block';
102 document.getElementById('passkey-register-btn').addEventListener('click', async () => {
103 const username = usernameInput.value.trim();
104 if (!username) { usernameInput.focus(); return; }
105 if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
106 alert('Username may only contain letters, numbers, hyphens, and underscores');
107 return;
108 }
109 if (applicationInput && !applicationInput.value.trim()) {
110 applicationInput.focus();
111 return;
112 }
113 try {
114 const createResp = await fetch('/auth/passkey/create-user', {
115 method: 'POST',
116 headers: { 'Content-Type': 'application/json' },
117 body: JSON.stringify({
118 username,
119 application: applicationInput ? applicationInput.value.trim() : undefined,
120 }),
121 });
122 if (!createResp.ok) {
123 const err = await createResp.json();
124 alert(err.error ?? 'Failed to create account');
125 return;
126 }
127 const data = await createResp.json();
128 if (data.pending) {
129 document.querySelector('.auth-container').innerHTML =
130 '<h1 class="page-title">Create account</h1>' +
131 '<p class="form-success">Your account has been submitted for review. You will be able to log in once an admin approves it.</p>' +
132 '<p class="auth-footer">Already have an account? <a href="/login">Sign in</a></p>';
133 return;
134 }
135 const optsResp = await fetch('/auth/passkey/register/options', { method: 'POST' });
136 const opts = await optsResp.json();
137 const result = await startRegistration({ optionsJSON: opts });
138 const verResp = await fetch('/auth/passkey/register/verify', {
139 method: 'POST',
140 headers: { 'Content-Type': 'application/json' },
141 body: JSON.stringify(result),
142 });
143 if (verResp.ok) {
144 window.location.href = '/';
145 } else {
146 const err = await verResp.json();
147 alert(err.error ?? 'Passkey registration failed');
148 }
149 } catch (e) {
150 alert('Passkey registration failed: ' + e.message);
151 }
152 });
153 `}</script>
154 </Layout>
155 );
156}
157