MasterPasswordSetup.svelte
1 <script lang="ts"> 2 /** 3 * Master Password Setup Component 4 * 5 * First-time setup for credential encryption. 6 * Prompts user to create a master password with confirmation. 7 */ 8 9 interface Props { 10 onSetup: () => void; 11 onCancel?: () => void; 12 } 13 14 let { onSetup, onCancel }: Props = $props(); 15 16 let password = $state(''); 17 let confirmPassword = $state(''); 18 let setting = $state(false); 19 let error = $state<string | null>(null); 20 21 // Password strength 22 const strength = $derived(calculateStrength(password)); 23 24 function calculateStrength(pwd: string): { level: 'weak' | 'fair' | 'good' | 'strong'; score: number } { 25 let score = 0; 26 27 if (pwd.length >= 8) score++; 28 if (pwd.length >= 12) score++; 29 if (pwd.length >= 16) score++; 30 if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score++; 31 if (/\d/.test(pwd)) score++; 32 if (/[^a-zA-Z\d]/.test(pwd)) score++; 33 34 if (score <= 1) return { level: 'weak', score: 25 }; 35 if (score <= 2) return { level: 'fair', score: 50 }; 36 if (score <= 4) return { level: 'good', score: 75 }; 37 return { level: 'strong', score: 100 }; 38 } 39 40 function validate(): string | null { 41 if (password.length < 8) { 42 return 'Password must be at least 8 characters'; 43 } 44 if (password !== confirmPassword) { 45 return 'Passwords do not match'; 46 } 47 return null; 48 } 49 50 async function handleSetup() { 51 const validationError = validate(); 52 if (validationError) { 53 error = validationError; 54 return; 55 } 56 57 setting = true; 58 error = null; 59 60 try { 61 const response = await browser.runtime.sendMessage({ 62 type: 'VAULT_SETUP', 63 payload: { password: password.trim() }, 64 }); 65 66 if (response.success) { 67 onSetup(); 68 } else { 69 error = response.error || 'Failed to set up master password'; 70 } 71 } catch (e) { 72 error = e instanceof Error ? e.message : 'Failed to set up master password'; 73 } finally { 74 setting = false; 75 } 76 } 77 78 function handleKeydown(e: KeyboardEvent) { 79 if (e.key === 'Enter' && !setting && password && confirmPassword) { 80 handleSetup(); 81 } 82 } 83 </script> 84 85 <div class="setup-container"> 86 <div class="setup-header"> 87 <div class="shield-icon"> 88 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 89 <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> 90 <path d="M9 12l2 2 4-4"/> 91 </svg> 92 </div> 93 <h3>Secure Your Credentials</h3> 94 <p class="setup-description"> 95 Create a master password to encrypt your API keys and integration credentials. 96 This password will be used to unlock your credentials on any device. 97 </p> 98 </div> 99 100 <div class="setup-form"> 101 <div class="input-group"> 102 <label for="new-password">Master Password</label> 103 <input 104 id="new-password" 105 type="password" 106 bind:value={password} 107 onkeydown={handleKeydown} 108 placeholder="Create a strong password" 109 class="password-input" 110 disabled={setting} 111 /> 112 {#if password.length > 0} 113 <div class="strength-indicator"> 114 <div class="strength-bar"> 115 <div 116 class="strength-fill strength-{strength.level}" 117 style="width: {strength.score}%" 118 ></div> 119 </div> 120 <span class="strength-label strength-{strength.level}"> 121 {strength.level.charAt(0).toUpperCase() + strength.level.slice(1)} 122 </span> 123 </div> 124 {/if} 125 </div> 126 127 <div class="input-group"> 128 <label for="confirm-password">Confirm Password</label> 129 <input 130 id="confirm-password" 131 type="password" 132 bind:value={confirmPassword} 133 onkeydown={handleKeydown} 134 placeholder="Confirm your password" 135 class="password-input" 136 disabled={setting} 137 /> 138 {#if confirmPassword && password !== confirmPassword} 139 <span class="mismatch-warning">Passwords do not match</span> 140 {/if} 141 </div> 142 143 {#if error} 144 <div class="error-message"> 145 <svg viewBox="0 0 20 20" fill="currentColor"> 146 <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 147 </svg> 148 <span>{error}</span> 149 </div> 150 {/if} 151 152 <div class="warning-box"> 153 <svg viewBox="0 0 20 20" fill="currentColor"> 154 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 155 </svg> 156 <div> 157 <strong>Important:</strong> This password cannot be recovered. If you forget it, 158 you will need to re-enter all your integration credentials. 159 </div> 160 </div> 161 162 <div class="setup-actions"> 163 {#if onCancel} 164 <button type="button" class="btn-secondary" onclick={onCancel} disabled={setting}> 165 Cancel 166 </button> 167 {/if} 168 <button 169 type="button" 170 class="btn-primary" 171 onclick={handleSetup} 172 disabled={setting || !password || !confirmPassword || password !== confirmPassword} 173 > 174 {#if setting} 175 Setting up... 176 {:else} 177 Create Master Password 178 {/if} 179 </button> 180 </div> 181 </div> 182 </div> 183 184 <style> 185 .setup-container { 186 padding: 1.5rem; 187 background: var(--color-bg-light); 188 border-radius: 8px; 189 border: 1px solid rgba(217, 137, 46, 0.2); 190 } 191 192 .setup-header { 193 text-align: center; 194 margin-bottom: 1.5rem; 195 } 196 197 .shield-icon { 198 width: 48px; 199 height: 48px; 200 margin: 0 auto 1rem; 201 color: var(--color-phosphor); 202 } 203 204 .shield-icon svg { 205 width: 100%; 206 height: 100%; 207 } 208 209 .setup-header h3 { 210 margin: 0 0 0.5rem; 211 font-size: 1.125rem; 212 color: var(--color-text-primary); 213 } 214 215 .setup-description { 216 margin: 0; 217 font-size: 0.875rem; 218 color: var(--color-text-muted); 219 line-height: 1.5; 220 } 221 222 .setup-form { 223 display: flex; 224 flex-direction: column; 225 gap: 1rem; 226 } 227 228 .input-group { 229 display: flex; 230 flex-direction: column; 231 gap: 0.375rem; 232 } 233 234 .input-group label { 235 font-size: 0.875rem; 236 color: var(--color-text-secondary); 237 } 238 239 .password-input { 240 width: 100%; 241 padding: 0.75rem; 242 background: var(--color-bg-dark); 243 border: 1px solid rgba(217, 137, 46, 0.3); 244 border-radius: 6px; 245 color: var(--color-text-primary); 246 font-size: 1rem; 247 } 248 249 .password-input:focus { 250 outline: none; 251 border-color: var(--color-phosphor); 252 } 253 254 .password-input:disabled { 255 opacity: 0.6; 256 cursor: not-allowed; 257 } 258 259 .strength-indicator { 260 display: flex; 261 align-items: center; 262 gap: 0.5rem; 263 margin-top: 0.25rem; 264 } 265 266 .strength-bar { 267 flex: 1; 268 height: 4px; 269 background: rgba(217, 137, 46, 0.2); 270 border-radius: 2px; 271 overflow: hidden; 272 } 273 274 .strength-fill { 275 height: 100%; 276 transition: width 0.3s ease; 277 } 278 279 .strength-fill.strength-weak { 280 background: #ef4444; 281 } 282 283 .strength-fill.strength-fair { 284 background: #f59e0b; 285 } 286 287 .strength-fill.strength-good { 288 background: #10b981; 289 } 290 291 .strength-fill.strength-strong { 292 background: #22c55e; 293 } 294 295 .strength-label { 296 font-size: 0.75rem; 297 font-weight: 500; 298 } 299 300 .strength-label.strength-weak { 301 color: #ef4444; 302 } 303 304 .strength-label.strength-fair { 305 color: #f59e0b; 306 } 307 308 .strength-label.strength-good { 309 color: #10b981; 310 } 311 312 .strength-label.strength-strong { 313 color: #22c55e; 314 } 315 316 .mismatch-warning { 317 font-size: 0.75rem; 318 color: #ef4444; 319 } 320 321 .error-message { 322 display: flex; 323 align-items: center; 324 gap: 0.5rem; 325 padding: 0.75rem; 326 background: rgba(239, 68, 68, 0.1); 327 border-radius: 6px; 328 color: #ef4444; 329 font-size: 0.875rem; 330 } 331 332 .error-message svg { 333 width: 1rem; 334 height: 1rem; 335 flex-shrink: 0; 336 } 337 338 .warning-box { 339 display: flex; 340 gap: 0.75rem; 341 padding: 0.75rem; 342 background: rgba(245, 158, 11, 0.1); 343 border: 1px solid rgba(245, 158, 11, 0.3); 344 border-radius: 6px; 345 font-size: 0.8125rem; 346 color: var(--color-text-secondary); 347 } 348 349 .warning-box svg { 350 width: 1.25rem; 351 height: 1.25rem; 352 color: #f59e0b; 353 flex-shrink: 0; 354 margin-top: 0.125rem; 355 } 356 357 .warning-box strong { 358 color: #f59e0b; 359 } 360 361 .setup-actions { 362 display: flex; 363 justify-content: flex-end; 364 gap: 0.75rem; 365 margin-top: 0.5rem; 366 } 367 368 .btn-primary { 369 padding: 0.75rem 1.5rem; 370 background: var(--color-phosphor); 371 color: var(--color-bg); 372 border: none; 373 border-radius: 6px; 374 font-weight: 600; 375 cursor: pointer; 376 transition: background-color 0.2s; 377 } 378 379 .btn-primary:hover:not(:disabled) { 380 background: var(--color-phosphor-light); 381 } 382 383 .btn-primary:disabled { 384 opacity: 0.6; 385 cursor: not-allowed; 386 } 387 388 .btn-secondary { 389 padding: 0.75rem 1.5rem; 390 background: transparent; 391 color: var(--color-text-secondary); 392 border: 1px solid rgba(217, 137, 46, 0.3); 393 border-radius: 6px; 394 font-weight: 500; 395 cursor: pointer; 396 transition: all 0.2s; 397 } 398 399 .btn-secondary:hover:not(:disabled) { 400 border-color: var(--color-phosphor); 401 color: var(--color-phosphor); 402 } 403 404 .btn-secondary:disabled { 405 opacity: 0.6; 406 cursor: not-allowed; 407 } 408 </style>