LoginModal.svelte raw

   1  <script>
   2      import { createEventDispatcher, onMount } from 'svelte';
   3      import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
   4      import { nsecEncode, npubEncode, decode } from 'nostr-tools/nip19';
   5      import { finalizeEvent } from 'nostr-tools/pure';
   6  
   7      const dispatch = createEventDispatcher();
   8  
   9      export let showModal = false;
  10      export let isDarkTheme = false;
  11  
  12      let activeTab = 'extension';
  13      let nsecInput = '';
  14      let isLoading = false;
  15      let errorMessage = '';
  16      let successMessage = '';
  17      let generatedNsec = '';
  18      let generatedNpub = '';
  19  
  20      function closeModal() {
  21          showModal = false;
  22          nsecInput = '';
  23          errorMessage = '';
  24          successMessage = '';
  25          generatedNsec = '';
  26          generatedNpub = '';
  27          dispatch('close');
  28      }
  29  
  30      function switchTab(tab) {
  31          activeTab = tab;
  32          errorMessage = '';
  33          successMessage = '';
  34          generatedNsec = '';
  35          generatedNpub = '';
  36      }
  37  
  38      async function generateNewKey() {
  39          errorMessage = '';
  40          successMessage = '';
  41  
  42          try {
  43              const secretKey = generateSecretKey();
  44              const nsec = nsecEncode(secretKey);
  45              const pubkey = getPublicKey(secretKey);
  46              const npub = npubEncode(pubkey);
  47  
  48              generatedNsec = nsec;
  49              generatedNpub = npub;
  50              nsecInput = nsec;
  51  
  52              successMessage = 'New key generated!';
  53          } catch (error) {
  54              errorMessage = 'Failed to generate key: ' + error.message;
  55          }
  56      }
  57  
  58      async function loginWithExtension() {
  59          isLoading = true;
  60          errorMessage = '';
  61          successMessage = '';
  62  
  63          try {
  64              if (!window.nostr) {
  65                  throw new Error('No Nostr extension found. Please install nos2x or Alby.');
  66              }
  67  
  68              const pubkey = await window.nostr.getPublicKey();
  69  
  70              if (pubkey) {
  71                  successMessage = 'Successfully logged in with extension!';
  72                  dispatch('login', {
  73                      method: 'extension',
  74                      pubkey: pubkey,
  75                      signer: window.nostr,
  76                  });
  77  
  78                  setTimeout(closeModal, 500);
  79              }
  80          } catch (error) {
  81              errorMessage = error.message;
  82          } finally {
  83              isLoading = false;
  84          }
  85      }
  86  
  87      async function loginWithNsec() {
  88          isLoading = true;
  89          errorMessage = '';
  90          successMessage = '';
  91  
  92          try {
  93              if (!nsecInput.trim()) {
  94                  throw new Error('Please enter your nsec');
  95              }
  96  
  97              const trimmed = nsecInput.trim();
  98  
  99              // Decode nsec
 100              let decoded;
 101              try {
 102                  decoded = decode(trimmed);
 103              } catch {
 104                  throw new Error('Invalid nsec format');
 105              }
 106  
 107              if (decoded.type !== 'nsec') {
 108                  throw new Error('Please enter an nsec (private key)');
 109              }
 110  
 111              const secretKey = decoded.data;
 112              const publicKey = getPublicKey(secretKey);
 113  
 114              // Create a signer that uses the secret key
 115              const signer = {
 116                  getPublicKey: async () => publicKey,
 117                  signEvent: async (event) => {
 118                      return finalizeEvent(event, secretKey);
 119                  }
 120              };
 121  
 122              successMessage = 'Successfully logged in!';
 123              dispatch('login', {
 124                  method: 'nsec',
 125                  pubkey: publicKey,
 126                  privateKey: trimmed,
 127                  signer: signer,
 128              });
 129  
 130              setTimeout(closeModal, 500);
 131          } catch (error) {
 132              errorMessage = error.message;
 133          } finally {
 134              isLoading = false;
 135          }
 136      }
 137  
 138      function handleKeydown(event) {
 139          if (event.key === 'Escape') {
 140              closeModal();
 141          }
 142          if (event.key === 'Enter' && activeTab === 'nsec') {
 143              loginWithNsec();
 144          }
 145      }
 146  </script>
 147  
 148  <svelte:window on:keydown={handleKeydown} />
 149  
 150  {#if showModal}
 151      <div
 152          class="modal-overlay"
 153          on:click={closeModal}
 154          on:keydown={(e) => e.key === 'Escape' && closeModal()}
 155          role="button"
 156          tabindex="0"
 157      >
 158          <div
 159              class="modal"
 160              class:dark-theme={isDarkTheme}
 161              on:click|stopPropagation
 162              on:keydown|stopPropagation
 163          >
 164              <div class="modal-header">
 165                  <h2>Login to Launcher Admin</h2>
 166                  <button class="close-btn" on:click={closeModal}>&times;</button>
 167              </div>
 168  
 169              <div class="tab-container">
 170                  <div class="tabs">
 171                      <button
 172                          class="tab-btn"
 173                          class:active={activeTab === 'extension'}
 174                          on:click={() => switchTab('extension')}
 175                      >
 176                          Extension
 177                      </button>
 178                      <button
 179                          class="tab-btn"
 180                          class:active={activeTab === 'nsec'}
 181                          on:click={() => switchTab('nsec')}
 182                      >
 183                          Nsec
 184                      </button>
 185                  </div>
 186  
 187                  <div class="tab-content">
 188                      {#if activeTab === 'extension'}
 189                          <div class="extension-login">
 190                              <p>Login using a NIP-07 browser extension like nos2x or Alby.</p>
 191                              <button
 192                                  class="login-btn"
 193                                  on:click={loginWithExtension}
 194                                  disabled={isLoading}
 195                              >
 196                                  {isLoading ? 'Connecting...' : 'Login with Extension'}
 197                              </button>
 198                          </div>
 199                      {:else}
 200                          <div class="nsec-login">
 201                              <p>Enter your nsec or generate a new key pair.</p>
 202  
 203                              <button
 204                                  class="generate-btn"
 205                                  on:click={generateNewKey}
 206                                  disabled={isLoading}
 207                              >
 208                                  Generate New Key
 209                              </button>
 210  
 211                              {#if generatedNpub}
 212                                  <div class="generated-info">
 213                                      <label>Your new public key (npub):</label>
 214                                      <code>{generatedNpub}</code>
 215                                  </div>
 216                              {/if}
 217  
 218                              <input
 219                                  type="password"
 220                                  placeholder="nsec1..."
 221                                  bind:value={nsecInput}
 222                                  disabled={isLoading}
 223                                  class="nsec-input"
 224                              />
 225  
 226                              <button
 227                                  class="login-btn"
 228                                  on:click={loginWithNsec}
 229                                  disabled={isLoading || !nsecInput.trim()}
 230                              >
 231                                  {isLoading ? 'Logging in...' : 'Login with Nsec'}
 232                              </button>
 233                          </div>
 234                      {/if}
 235  
 236                      {#if errorMessage}
 237                          <div class="message error-message">{errorMessage}</div>
 238                      {/if}
 239  
 240                      {#if successMessage}
 241                          <div class="message success-message">{successMessage}</div>
 242                      {/if}
 243                  </div>
 244              </div>
 245          </div>
 246      </div>
 247  {/if}
 248  
 249  <style>
 250      .modal-overlay {
 251          position: fixed;
 252          top: 0;
 253          left: 0;
 254          width: 100%;
 255          height: 100%;
 256          background-color: rgba(0, 0, 0, 0.5);
 257          display: flex;
 258          justify-content: center;
 259          align-items: center;
 260          z-index: 1000;
 261      }
 262  
 263      .modal {
 264          background: var(--card-bg, #fff);
 265          border-radius: 8px;
 266          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
 267          width: 90%;
 268          max-width: 450px;
 269          border: 1px solid var(--border-color, #e0e0e0);
 270      }
 271  
 272      .modal-header {
 273          display: flex;
 274          justify-content: space-between;
 275          align-items: center;
 276          padding: 20px;
 277          border-bottom: 1px solid var(--border-color, #e0e0e0);
 278      }
 279  
 280      .modal-header h2 {
 281          margin: 0;
 282          color: var(--text-color, #333);
 283          font-size: 1.25rem;
 284      }
 285  
 286      .close-btn {
 287          background: none;
 288          border: none;
 289          font-size: 1.5rem;
 290          cursor: pointer;
 291          color: var(--text-color, #333);
 292          padding: 0;
 293          width: 30px;
 294          height: 30px;
 295          display: flex;
 296          align-items: center;
 297          justify-content: center;
 298          border-radius: 50%;
 299      }
 300  
 301      .close-btn:hover {
 302          background-color: var(--border-color, #e0e0e0);
 303      }
 304  
 305      .tab-container {
 306          padding: 20px;
 307      }
 308  
 309      .tabs {
 310          display: flex;
 311          border-bottom: 1px solid var(--border-color, #e0e0e0);
 312          margin-bottom: 20px;
 313      }
 314  
 315      .tab-btn {
 316          flex: 1;
 317          padding: 12px 16px;
 318          background: none;
 319          border: none;
 320          cursor: pointer;
 321          color: var(--text-color, #333);
 322          font-size: 1rem;
 323          border-bottom: 2px solid transparent;
 324      }
 325  
 326      .tab-btn:hover {
 327          background-color: var(--border-color, #e0e0e0);
 328      }
 329  
 330      .tab-btn.active {
 331          border-bottom-color: var(--primary, #00bcd4);
 332          color: var(--primary, #00bcd4);
 333      }
 334  
 335      .tab-content {
 336          min-height: 180px;
 337      }
 338  
 339      .extension-login,
 340      .nsec-login {
 341          display: flex;
 342          flex-direction: column;
 343          gap: 16px;
 344      }
 345  
 346      .extension-login p,
 347      .nsec-login p {
 348          margin: 0;
 349          color: var(--muted-color, #666);
 350          line-height: 1.5;
 351      }
 352  
 353      .login-btn {
 354          padding: 12px 24px;
 355          background: var(--primary, #00bcd4);
 356          color: white;
 357          border: none;
 358          border-radius: 6px;
 359          cursor: pointer;
 360          font-size: 1rem;
 361      }
 362  
 363      .login-btn:hover:not(:disabled) {
 364          background: var(--primary-hover, #00acc1);
 365      }
 366  
 367      .login-btn:disabled {
 368          background: #ccc;
 369          cursor: not-allowed;
 370      }
 371  
 372      .nsec-input {
 373          padding: 12px;
 374          border: 1px solid var(--border-color, #e0e0e0);
 375          border-radius: 6px;
 376          font-size: 1rem;
 377          background: var(--card-bg, #fff);
 378          color: var(--text-color, #333);
 379      }
 380  
 381      .nsec-input:focus {
 382          outline: none;
 383          border-color: var(--primary, #00bcd4);
 384      }
 385  
 386      .generate-btn {
 387          padding: 10px 20px;
 388          background: var(--success, #4caf50);
 389          color: white;
 390          border: none;
 391          border-radius: 6px;
 392          cursor: pointer;
 393          font-size: 0.95rem;
 394      }
 395  
 396      .generate-btn:hover:not(:disabled) {
 397          opacity: 0.9;
 398      }
 399  
 400      .generate-btn:disabled {
 401          background: #ccc;
 402          cursor: not-allowed;
 403      }
 404  
 405      .generated-info {
 406          background: var(--bg-color, #f5f5f5);
 407          padding: 12px;
 408          border-radius: 6px;
 409          border: 1px solid var(--border-color, #e0e0e0);
 410      }
 411  
 412      .generated-info label {
 413          display: block;
 414          font-size: 0.85rem;
 415          color: var(--muted-color, #666);
 416          margin-bottom: 6px;
 417      }
 418  
 419      .generated-info code {
 420          display: block;
 421          word-break: break-all;
 422          font-size: 0.8rem;
 423          color: var(--text-color, #333);
 424      }
 425  
 426      .message {
 427          padding: 10px;
 428          border-radius: 4px;
 429          margin-top: 16px;
 430          text-align: center;
 431      }
 432  
 433      .error-message {
 434          background: #ffebee;
 435          color: #c62828;
 436          border: 1px solid #ffcdd2;
 437      }
 438  
 439      .success-message {
 440          background: #e8f5e9;
 441          color: #2e7d32;
 442          border: 1px solid #c8e6c9;
 443      }
 444  
 445      .dark-theme .error-message {
 446          background: #4a2c2a;
 447          color: #ffcdd2;
 448      }
 449  
 450      .dark-theme .success-message {
 451          background: #2e4a2e;
 452          color: #a5d6a7;
 453      }
 454  </style>
 455