this repo has no description
1<script lang="ts">
2 import { register, confirmSignup, resendVerification, getAuthState } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { api, ApiError, type VerificationChannel } from '../lib/api'
5
6 let handle = $state('')
7 let email = $state('')
8 let password = $state('')
9 let confirmPassword = $state('')
10 let inviteCode = $state('')
11 let verificationChannel = $state<VerificationChannel>('email')
12 let discordId = $state('')
13 let telegramUsername = $state('')
14 let signalNumber = $state('')
15 let submitting = $state(false)
16 let error = $state<string | null>(null)
17
18 let pendingVerification = $state<{ did: string; handle: string; channel: string } | null>(null)
19 let verificationCode = $state('')
20 let resendingCode = $state(false)
21 let resendMessage = $state<string | null>(null)
22
23 let serverInfo = $state<{
24 availableUserDomains: string[]
25 inviteCodeRequired: boolean
26 } | null>(null)
27 let loadingServerInfo = $state(true)
28 let serverInfoLoaded = false
29
30 const auth = getAuthState()
31
32 $effect(() => {
33 if (auth.session) {
34 navigate('/dashboard')
35 }
36 })
37
38 $effect(() => {
39 if (!serverInfoLoaded) {
40 serverInfoLoaded = true
41 loadServerInfo()
42 }
43 })
44
45 async function loadServerInfo() {
46 try {
47 serverInfo = await api.describeServer()
48 } catch (e) {
49 console.error('Failed to load server info:', e)
50 } finally {
51 loadingServerInfo = false
52 }
53 }
54
55 function validateForm(): string | null {
56 if (!handle.trim()) return 'Handle is required'
57 if (!password) return 'Password is required'
58 if (password.length < 8) return 'Password must be at least 8 characters'
59 if (password !== confirmPassword) return 'Passwords do not match'
60 if (serverInfo?.inviteCodeRequired && !inviteCode.trim()) {
61 return 'Invite code is required'
62 }
63 switch (verificationChannel) {
64 case 'email':
65 if (!email.trim()) return 'Email is required for email verification'
66 break
67 case 'discord':
68 if (!discordId.trim()) return 'Discord ID is required for Discord verification'
69 break
70 case 'telegram':
71 if (!telegramUsername.trim()) return 'Telegram username is required for Telegram verification'
72 break
73 case 'signal':
74 if (!signalNumber.trim()) return 'Phone number is required for Signal verification'
75 break
76 }
77 return null
78 }
79
80 async function handleSubmit(e: Event) {
81 e.preventDefault()
82 console.log('[Register] handleSubmit called')
83
84 const validationError = validateForm()
85 if (validationError) {
86 console.log('[Register] validation error:', validationError)
87 error = validationError
88 return
89 }
90
91 submitting = true
92 error = null
93 console.log('[Register] starting registration...')
94
95 try {
96 const result = await register({
97 handle: handle.trim(),
98 email: email.trim(),
99 password,
100 inviteCode: inviteCode.trim() || undefined,
101 verificationChannel,
102 discordId: discordId.trim() || undefined,
103 telegramUsername: telegramUsername.trim() || undefined,
104 signalNumber: signalNumber.trim() || undefined,
105 })
106 console.log('[Register] registration result:', result)
107
108 if (result.verificationRequired) {
109 console.log('[Register] setting pendingVerification')
110 pendingVerification = {
111 did: result.did,
112 handle: result.handle,
113 channel: result.verificationChannel,
114 }
115 console.log('[Register] pendingVerification set to:', pendingVerification)
116 } else {
117 console.log('[Register] no verification required, navigating to dashboard')
118 navigate('/dashboard')
119 }
120 } catch (err: any) {
121 console.error('[Register] error:', err)
122 if (err instanceof ApiError) {
123 error = err.message || 'Registration failed'
124 } else if (err instanceof Error) {
125 error = err.message || 'Registration failed'
126 } else {
127 error = 'Registration failed'
128 }
129 } finally {
130 submitting = false
131 console.log('[Register] finished, submitting=false')
132 }
133 }
134
135 async function handleVerification(e: Event) {
136 e.preventDefault()
137
138 if (!pendingVerification || !verificationCode.trim()) return
139
140 submitting = true
141 error = null
142
143 try {
144 await confirmSignup(pendingVerification.did, verificationCode.trim())
145 navigate('/dashboard')
146 } catch (e: any) {
147 error = e.message || 'Verification failed'
148 } finally {
149 submitting = false
150 }
151 }
152
153 async function handleResendCode() {
154 if (!pendingVerification || resendingCode) return
155
156 resendingCode = true
157 resendMessage = null
158 error = null
159
160 try {
161 await resendVerification(pendingVerification.did)
162 resendMessage = 'Verification code resent!'
163 } catch (e: any) {
164 error = e.message || 'Failed to resend code'
165 } finally {
166 resendingCode = false
167 }
168 }
169
170 let fullHandle = $derived(() => {
171 if (!handle.trim()) return ''
172 if (handle.includes('.')) return handle.trim()
173 const domain = serverInfo?.availableUserDomains?.[0]
174 if (domain) return `${handle.trim()}.${domain}`
175 return handle.trim()
176 })
177
178 function channelLabel(ch: string): string {
179 switch (ch) {
180 case 'email': return 'Email'
181 case 'discord': return 'Discord'
182 case 'telegram': return 'Telegram'
183 case 'signal': return 'Signal'
184 default: return ch
185 }
186 }
187</script>
188
189<div class="register-container">
190 {#if error}
191 <div class="error">{error}</div>
192 {/if}
193
194 {#if pendingVerification}
195 <h1>Verify Your Account</h1>
196 <p class="subtitle">
197 We've sent a verification code to your {channelLabel(pendingVerification.channel)}.
198 Enter it below to complete registration.
199 </p>
200
201 {#if resendMessage}
202 <div class="success">{resendMessage}</div>
203 {/if}
204
205 <form onsubmit={(e) => { e.preventDefault(); handleVerification(e); }}>
206
207 <div class="field">
208 <label for="verification-code">Verification Code</label>
209 <input
210 id="verification-code"
211 type="text"
212 bind:value={verificationCode}
213 placeholder="Enter 6-digit code"
214 disabled={submitting}
215 required
216 maxlength="6"
217 inputmode="numeric"
218 autocomplete="one-time-code"
219 />
220 </div>
221
222 <button type="submit" disabled={submitting || !verificationCode.trim()}>
223 {submitting ? 'Verifying...' : 'Verify Account'}
224 </button>
225
226 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}>
227 {resendingCode ? 'Resending...' : 'Resend Code'}
228 </button>
229 </form>
230 {:else}
231 <h1>Create Account</h1>
232 <p class="subtitle">Create a new account on this PDS</p>
233
234 {#if loadingServerInfo}
235 <p class="loading">Loading...</p>
236 {:else}
237 <form onsubmit={(e) => { e.preventDefault(); handleSubmit(e); }}>
238 <div class="field">
239 <label for="handle">Handle</label>
240 <input
241 id="handle"
242 type="text"
243 bind:value={handle}
244 placeholder="yourname"
245 disabled={submitting}
246 required
247 />
248 {#if fullHandle()}
249 <p class="hint">Your full handle will be: @{fullHandle()}</p>
250 {/if}
251 </div>
252
253 <div class="field">
254 <label for="password">Password</label>
255 <input
256 id="password"
257 type="password"
258 bind:value={password}
259 placeholder="At least 8 characters"
260 disabled={submitting}
261 required
262 minlength="8"
263 />
264 </div>
265
266 <div class="field">
267 <label for="confirm-password">Confirm Password</label>
268 <input
269 id="confirm-password"
270 type="password"
271 bind:value={confirmPassword}
272 placeholder="Confirm your password"
273 disabled={submitting}
274 required
275 />
276 </div>
277
278 <fieldset class="verification-section">
279 <legend>Contact Method</legend>
280 <p class="section-hint">Choose how you'd like to verify your account and receive notifications. You only need one.</p>
281
282 <div class="field">
283 <label for="verification-channel">Verification Method</label>
284 <select
285 id="verification-channel"
286 bind:value={verificationChannel}
287 disabled={submitting}
288 >
289 <option value="email">Email</option>
290 <option value="discord">Discord</option>
291 <option value="telegram">Telegram</option>
292 <option value="signal">Signal</option>
293 </select>
294 </div>
295
296 {#if verificationChannel === 'email'}
297 <div class="field">
298 <label for="email">Email Address</label>
299 <input
300 id="email"
301 type="email"
302 bind:value={email}
303 placeholder="you@example.com"
304 disabled={submitting}
305 required
306 />
307 </div>
308 {:else if verificationChannel === 'discord'}
309 <div class="field">
310 <label for="discord-id">Discord User ID</label>
311 <input
312 id="discord-id"
313 type="text"
314 bind:value={discordId}
315 placeholder="Your Discord user ID"
316 disabled={submitting}
317 required
318 />
319 <p class="hint">Your numeric Discord user ID (enable Developer Mode to find it)</p>
320 </div>
321 {:else if verificationChannel === 'telegram'}
322 <div class="field">
323 <label for="telegram-username">Telegram Username</label>
324 <input
325 id="telegram-username"
326 type="text"
327 bind:value={telegramUsername}
328 placeholder="@yourusername"
329 disabled={submitting}
330 required
331 />
332 </div>
333 {:else if verificationChannel === 'signal'}
334 <div class="field">
335 <label for="signal-number">Signal Phone Number</label>
336 <input
337 id="signal-number"
338 type="tel"
339 bind:value={signalNumber}
340 placeholder="+1234567890"
341 disabled={submitting}
342 required
343 />
344 <p class="hint">Include country code (e.g., +1 for US)</p>
345 </div>
346 {/if}
347 </fieldset>
348
349 {#if serverInfo?.inviteCodeRequired}
350 <div class="field">
351 <label for="invite-code">Invite Code <span class="required">*</span></label>
352 <input
353 id="invite-code"
354 type="text"
355 bind:value={inviteCode}
356 placeholder="Enter your invite code"
357 disabled={submitting}
358 required
359 />
360 </div>
361 {:else}
362 <div class="field optional">
363 <label for="invite-code">Invite Code <span class="optional-label">(optional)</span></label>
364 <input
365 id="invite-code"
366 type="text"
367 bind:value={inviteCode}
368 placeholder="Enter invite code if you have one"
369 disabled={submitting}
370 />
371 </div>
372 {/if}
373
374 <button type="submit" disabled={submitting}>
375 {submitting ? 'Creating account...' : 'Create Account'}
376 </button>
377 </form>
378
379 <p class="login-link">
380 Already have an account? <a href="#/login">Sign in</a>
381 </p>
382 {/if}
383 {/if}
384</div>
385
386<style>
387 .register-container {
388 max-width: 400px;
389 margin: 4rem auto;
390 padding: 2rem;
391 }
392
393 h1 {
394 margin: 0 0 0.5rem 0;
395 }
396
397 .subtitle {
398 color: var(--text-secondary);
399 margin: 0 0 2rem 0;
400 }
401
402 .loading {
403 text-align: center;
404 color: var(--text-secondary);
405 }
406
407 form {
408 display: flex;
409 flex-direction: column;
410 gap: 1rem;
411 }
412
413 .field {
414 display: flex;
415 flex-direction: column;
416 gap: 0.25rem;
417 }
418
419 .field.optional {
420 opacity: 0.8;
421 }
422
423 label {
424 font-size: 0.875rem;
425 font-weight: 500;
426 }
427
428 .required {
429 color: var(--error-text);
430 }
431
432 .optional-label {
433 color: var(--text-secondary);
434 font-weight: normal;
435 }
436
437 input, select {
438 padding: 0.75rem;
439 border: 1px solid var(--border-color-light);
440 border-radius: 4px;
441 font-size: 1rem;
442 background: var(--bg-input);
443 color: var(--text-primary);
444 }
445
446 input:focus, select:focus {
447 outline: none;
448 border-color: var(--accent);
449 }
450
451 .hint {
452 font-size: 0.75rem;
453 color: var(--text-secondary);
454 margin: 0.25rem 0 0 0;
455 }
456
457 .verification-section {
458 border: 1px solid var(--border-color-light);
459 border-radius: 6px;
460 padding: 1rem;
461 margin: 0.5rem 0;
462 }
463
464 .verification-section legend {
465 font-weight: 600;
466 padding: 0 0.5rem;
467 color: var(--text-primary);
468 }
469
470 .section-hint {
471 font-size: 0.8rem;
472 color: var(--text-secondary);
473 margin: 0 0 1rem 0;
474 }
475
476 button {
477 padding: 0.75rem;
478 background: var(--accent);
479 color: white;
480 border: none;
481 border-radius: 4px;
482 font-size: 1rem;
483 cursor: pointer;
484 margin-top: 0.5rem;
485 }
486
487 button:hover:not(:disabled) {
488 background: var(--accent-hover);
489 }
490
491 button:disabled {
492 opacity: 0.6;
493 cursor: not-allowed;
494 }
495
496 button.secondary {
497 background: transparent;
498 color: var(--accent);
499 border: 1px solid var(--accent);
500 }
501
502 button.secondary:hover:not(:disabled) {
503 background: var(--accent);
504 color: white;
505 }
506
507 .error {
508 padding: 0.75rem;
509 background: var(--error-bg);
510 border: 1px solid var(--error-border);
511 border-radius: 4px;
512 color: var(--error-text);
513 }
514
515 .success {
516 padding: 0.75rem;
517 background: var(--success-bg);
518 border: 1px solid var(--success-border);
519 border-radius: 4px;
520 color: var(--success-text);
521 }
522
523 .login-link {
524 text-align: center;
525 margin-top: 1.5rem;
526 color: var(--text-secondary);
527 }
528
529 .login-link a {
530 color: var(--accent);
531 }
532</style>