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