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