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