view mrjunejune/src/account/password.html @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents 04fee26ecce0
children
line wrap: on
line source

<!DOCTYPE html>
<html lang="en">
<head>
  {{/parts/base_head.html}}
  <title>Change password — MrJuneJune</title>
  <style>
    main {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 60vh;
      padding: var(--zenbu-sys-padding-lg, 2rem) var(--zenbu-sys-padding-md, 1rem);
    }

    .password-card {
      width: 100%;
      max-width: 400px;
    }

    .password-card h2 {
      margin: 0 0 var(--zenbu-sys-padding-md, 1.25rem);
      font-family: "More", sans-serif;
      font-size: 1.4rem;
      font-weight: 700;
      text-align: center;
    }

    .password-form {
      display: flex;
      flex-direction: column;
      gap: var(--zenbu-sys-padding-sm, 0.75rem);
    }

    .password-actions {
      display: flex;
      justify-content: flex-end;
      margin-top: var(--zenbu-sys-padding-xs, 0.5rem);
    }

    #passwordError {
      display: none;
      padding: 0.6rem 0.75rem;
      border-radius: 6px;
      background: color-mix(in srgb, var(--zenbu-sys-color-error, #c0392b) 12%, transparent);
      color: var(--zenbu-sys-color-error, #c0392b);
      font-size: 0.875rem;
    }

    #passwordError[aria-hidden="false"] {
      display: block;
    }

    #passwordSuccess {
      display: none;
      padding: 0.6rem 0.75rem;
      border-radius: 6px;
      background: color-mix(in srgb, var(--zenbu-sys-color-success, #27ae60) 12%, transparent);
      color: var(--zenbu-sys-color-success, #27ae60);
      font-size: 0.875rem;
    }

    #passwordSuccess[aria-hidden="false"] {
      display: block;
    }
  </style>
</head>
<body>
  {{/parts/header.html}}

  <main>
    <div class="password-card">
      <zen-heading size="xl">
        <h2>Change password</h2>
      </zen-heading>

      <div id="passwordError" role="alert" aria-live="assertive" aria-hidden="true"></div>
      <div id="passwordSuccess" role="status" aria-live="polite" aria-hidden="true"></div>

      <form class="password-form" id="passwordForm" novalidate>
        <zen-field size="md">
          <label for="currentPassword">Current password</label>
          <input
            id="currentPassword"
            name="currentPassword"
            type="password"
            autocomplete="current-password"
            required
          >
        </zen-field>

        <zen-field size="md">
          <label for="newPassword">New password</label>
          <input
            id="newPassword"
            name="newPassword"
            type="password"
            autocomplete="new-password"
            required
            minlength="12"
            maxlength="1024"
          >
        </zen-field>

        <zen-field size="md">
          <label for="confirmPassword">Confirm new password</label>
          <input
            id="confirmPassword"
            name="confirmPassword"
            type="password"
            autocomplete="new-password"
            required
            minlength="12"
            maxlength="1024"
          >
        </zen-field>

        <div class="password-actions">
          <zen-button size="md">
            <button type="submit" id="passwordSubmit">Change password</button>
          </zen-button>
        </div>
      </form>
    </div>
  </main>

  <script>
    (function () {
      'use strict';

      const form        = document.getElementById('passwordForm');
      const errorEl     = document.getElementById('passwordError');
      const successEl   = document.getElementById('passwordSuccess');
      const submitBtn   = document.getElementById('passwordSubmit');

      function showError(message) {
        errorEl.textContent = message;
        errorEl.setAttribute('aria-hidden', 'false');
        successEl.setAttribute('aria-hidden', 'true');
      }

      function showSuccess(message) {
        successEl.textContent = message;
        successEl.setAttribute('aria-hidden', 'false');
        errorEl.setAttribute('aria-hidden', 'true');
      }

      function hideMessages() {
        errorEl.textContent = '';
        errorEl.setAttribute('aria-hidden', 'true');
        successEl.textContent = '';
        successEl.setAttribute('aria-hidden', 'true');
      }

      async function fetchSession() {
        const resp = await fetch('/api/auth/session', {
          credentials: 'same-origin',
        });
        if (!resp.ok) throw new Error('session unavailable');
        return resp.json();
      }

      form.addEventListener('submit', async function (evt) {
        evt.preventDefault();
        hideMessages();

        const currentPassword = form.currentPassword.value;
        const newPassword     = form.newPassword.value;
        const confirmPassword = form.confirmPassword.value;

        if (!currentPassword) {
          showError('Please enter your current password.');
          return;
        }
        if (!newPassword || newPassword.length < 12) {
          showError('New password must be at least 12 characters.');
          return;
        }
        if (newPassword !== confirmPassword) {
          showError('New passwords do not match.');
          return;
        }

        submitBtn.disabled = true;

        try {
          const session = await fetchSession();
          if (session.kind !== 'user') {
            showError('You must be signed in to change your password.');
            return;
          }

          const csrfToken = session.csrfToken;

          const resp = await fetch('/api/auth/password', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Origin': window.location.origin,
            },
            credentials: 'same-origin',
            body: JSON.stringify({ currentPassword, newPassword, csrfToken }),
          });

          const data = await resp.json();

          if (!resp.ok) {
            if (resp.status === 401) {
              showError('Current password is incorrect.');
            } else if (resp.status === 400 && data.error &&
                       data.error.code === 'password_policy') {
              showError(data.error.message ||
                        'New password must be at least 12 characters.');
            } else {
              showError('Password change failed. Please try again.');
            }
            return;
          }

          showSuccess('Password changed successfully.');
          form.reset();

          /* Redirect to home after a brief delay */
          setTimeout(function () {
            window.location.href = '/jrpg';
          }, 1500);

        } catch (_) {
          showError('Password change failed. Please try again.');
        } finally {
          submitBtn.disabled = false;
        }
      });
    })();
  </script>
</body>
</html>