/ src / components / credentials / MasterPasswordSetup.svelte
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>