view mrjunejune/src/account/password.html @ 265:056790c4fb0d

add role-aware Epi assistant prompts Add verified June knowledge, guest/member/admin Copilot profiles, profile-isolated session recovery, animated Epi greetings, and a single authoritative runtime config workflow for inference. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 10:50:30 -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>