diff mrjunejune/src/account/password.html @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/account/password.html	Fri Aug 07 07:34:12 2026 -0700
@@ -0,0 +1,237 @@
+<!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>