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>