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>